Peki ya LISP, eğer varsa, makro sistemlerin uygulanmasını kolaylaştırır?


21

SICP'den Şema öğreniyorum ve Scheme'i yapan şeyin büyük bir kısmının ve daha özel olarak LISP'in makro sistem olduğu izlenimini alıyorum. Ancak, makrolar derleme zamanında genişletildiğinden, insanlar neden C / Python / Java / niçin eşdeğer makro sistemler yapmıyorlar? Örneğin, biri pythonkomutu expand-macros | pythonya da her neyse onu bağlayabilir . Kod, makro sistemini kullanmayan insanlara taşınabilir olacaktı; kod yayınlanmadan önce makrolar genişletilecek. Ama topladığım C ++ / Haskell'deki şablonlar dışında böyle bir şey bilmiyorum, aslında aynı değil. Peki ya LISP, eğer varsa, makro sistemlerin uygulanmasını kolaylaştırır?


3
"Kod, makro sistemini kullanmayan insanlar için hala taşınabilir olacaktı; kod yayınlanmadan önce makrolar genişletilecek." - sadece sizi uyarmak için, bu iyi çalışma eğilimindedir. Bu diğer insanlar kodu çalıştırabilir, ancak pratikte makro-genişletilmiş kodun anlaşılması genellikle zor ve genellikle değiştirilmesi zor. Yazarın genişletilmiş kodunu insan gözlerine uyarlamaması anlamında “kötü yazılmış” dır, gerçek kaynağı uyarladı. Bir Java programcısına Java kodunuzu C önişlemcisinden geçirdiğinizi ve hangi rengin döndüklerini izlediğinizi söylemeyi deneyin ;-)
Steve Jessop

1
Ancak makroların yürütülmesi gerekir, bu noktada zaten dil için bir tercüman yazıyorsunuz.
Mehrdad

Yanıtlar:


29

Birçok Lispers size Lisp'i özel yapan şeyin homoikonluk olduğunu söyler , bu kodun sözdiziminin diğer verilerle aynı veri yapılarını kullanarak temsil edildiği anlamına gelir. Örneğin, dik açılı bir üçgenin hipotenüsünü verilen yan uzunluklarla hesaplamak için basit bir fonksiyon (Şema sözdizimini kullanarak) burada:

(define (hypot x y)
  (sqrt (+ (square x) (square y))))

Şimdi, homoiconicity, yukarıdaki kodun aslında Lisp kodunda bir veri yapısı (özellikle listelerin listeleri) olarak gösterilebileceğini söylüyor. Bu nedenle, aşağıdaki listeleri göz önünde bulundurun ve bunların nasıl "yapıştırıldığını" görün:

  1. (define #2# #3#)
  2. (hypot x y)
  3. (sqrt #4#)
  4. (+ #5# #6#)
  5. (square x)
  6. (square y)

Makrolar, kaynak kodunu sadece şu şekilde ele almanıza izin verir: malzeme listeleri. Bu 6 "sublists" her biri, diğer listeler ya işaretçiler içeren ya da semboller (bu örnekte: define, hypot, x, y, sqrt, +, square).


Peki, homoikonikliği sözdizimini “ayırmak” ve makro yapmak için nasıl kullanabiliriz? İşte basit bir örnek. Çağıracağımız letmakroyu yeniden uygulayalım my-let. Hatırlatma olarak,

(my-let ((foo 1)
         (bar 2))
  (+ foo bar))

genişletmek gerekir

((lambda (foo bar)
   (+ foo bar))
 1 2)

İşte şema "açıkça yeniden adlandırma" makrolarını kullanan bir uygulama :

(define-syntax my-let
  (er-macro-transformer
    (lambda (form rename compare)
      (define bindings (cadr form))
      (define body (cddr form))
      `((,(rename 'lambda) ,(map car bindings)
          ,@body)
        ,@(map cadr bindings)))))

formParametre gerçek forma bağlıdır, bu nedenle örneğin, olurdu (my-let ((foo 1) (bar 2)) (+ foo bar)). Öyleyse, örnek üzerinde çalışalım:

  1. İlk önce formları gelen ciltleri alırız. formun bir kısmını cadrkapar ((foo 1) (bar 2)).
  2. Sonra cesedi formdan alırız. formun bir kısmını cddrkapar ((+ foo bar)). (Bunun, bağlama işleminden sonra tüm alt formları alması amaçlandığını unutmayın;

    (my-let ((foo 1)
             (bar 2))
      (debug foo)
      (debug bar)
      (+ foo bar))
    

    o zaman vücut olur ((debug foo) (debug bar) (+ foo bar)).)

  3. Şimdi, aslında ortaya çıkan lambdaifadeyi inşa ediyoruz ve topladığımız bağlayıcıları ve bedeni kullanarak çağrı yapıyoruz. Tersine, "quasiquote" adı verilir; bu, quasiquote içindeki her şeyi virgül sonundaki bitler ("unquote") dışında , gerçek veriler olarak ele almak anlamına gelir .
    • (rename 'lambda)Aracı kullanmak için lambdabu makro edildiğinde yürürlükte bağlayıcı tanımlanan yerine şeyden daha lambdabu makro olduğunda etrafında olabilir bağlayıcı kullanılır . (Bu hijyen olarak bilinir .)
    • (map car bindings)döndürür (foo bar): her bir bağlantıdaki ilk veri.
    • (map cadr bindings)döner (1 2): bağlamaların her birinde ikinci veri.
    • ,@ Bir liste döndüren ifadeler için kullanılan "ekleme" yapar: listenin elemanlarının listenin kendisinden ziyade sonuca yapıştırılmasını sağlar.
  4. Bunları bir araya getirerek, sonuç olarak, burada yeniden adlandırılanların (($lambda (foo bar) (+ foo bar)) 1 2)bulunduğu listeyi alırız .$lambdalambda

Basit değil mi? ;-) (Sizin için kolay değilse, diğer diller için bir makro sistemi uygulamanın ne kadar zor olduğunu hayal edin.)


Böylece, diğer diller için makro sistemlerine sahip olabilirsiniz, eğer kaynak kodunu sıkışık olmayan bir şekilde "ayırabilecek" bir yönteminiz varsa. Bu konuda bazı girişimler var. Örneğin, sweet.js bunu JavaScript için yapar.

† Bunu okuyan tecrübeli Schemers için, açıkça başka adlandırma defmacrolehçeleri tarafından kullanılanlar arasında orta bir uzlaşma olarak açıkça yeniden adlandırma makroları kullanmayı seçtim ve syntax-rules(bu tür bir makroyu Scheme'de uygulamak için standart yol olurdu). Diğer Lisp lehçelerinde yazmak istemiyorum, ama alışkın olmayan Schemers'ı yabancılaştırmak istemiyorum syntax-rules.

Başvuru için, burada kullanılan my-letmakro syntax-rules:

(define-syntax my-let
  (syntax-rules ()
    ((my-let ((id val) ...)
       body ...)
     ((lambda (id ...)
        body ...)
      val ...))))

İlgili syntax-casesürüm çok benzer görünüyor:

(define-syntax my-let
  (lambda (stx)
    (syntax-case stx ()
      ((_ ((id val) ...)
         body ...)
       #'((lambda (id ...)
            body ...)
          val ...)))))

İkisi arasındaki fark yani her şeyin içinde syntax-rulesörtük vardır #'uygulamalı sen, böylece sadece içinde desen / şablon çiftleri var syntax-rulesdolayısıyla tamamen bildirim var. Aksine, syntax-casedesenden sonraki bit, sonunda bir sözdizimi nesnesini ( #'(...)) döndürmek zorunda olan , ancak başka kodlar da içerebilen gerçek koddur.


2
Bahsetmediğiniz bir avantaj: Evet, JS için sweet.js gibi başka dillerde de denemeler var. Ancak lisps'ta makro yazmak, fonksiyon yazmakla aynı dilde yapılır.
Florian Margaine

Doğru, işlemsel (bildirimsel) karşı makroları Lisp dillerinde yazabilirsiniz, bu da gerçekten gelişmiş şeyler yapmanızı sağlar. BTW, Scheme makro sistemleri hakkında sevdiğim şey şudur: aralarından seçim yapabileceğiniz birden fazla kişi var. Basit makrolar syntax-rulesiçin tamamen açıklayıcı olan kullanıyorum . Karmaşık makrolar syntax-caseiçin kısmen açıklayıcı ve kısmen prosedürel olan kullanabilirsiniz. Ve sonra tamamen prosedürel olan açık bir yeniden adlandırma var. (Çoğu Scheme uygulaması syntax-caseya ER ya da ER sağlayacaktır . Her ikisini de sağlayan birini görmedim. İktidarda eşdeğerdirler.)
Chris Jester-Young

Makrolar neden AST'yi değiştirmek zorunda? Neden daha yüksek bir seviyede çalışamıyorlar?
Elliot Gorokhovsky

1
Öyleyse neden LISP daha iyi? LISP'i özel yapan nedir? Makroları js cinsinden uygulayabiliyorsanız, kesinlikle herhangi biri başka bir dilde de uygulayabilir.
Elliot Gorokhovsky

3
@ RenéG ilk yorumumda dediğim gibi, büyük bir avantaj, hala aynı dilde yazıyor olmanız.
Florian Margaine

23

Muhalif bir fikir: Lisp'in eşcinselliği, Lisp hayranlarının inanacağından çok daha az faydalı bir şey.

Sözdizimsel makroları anlamak için derleyicileri anlamak önemlidir. Bir derleyicinin görevi, okunabilir kodu çalıştırılabilir koda dönüştürmektir. Çok yüksek bir perspektiften bakıldığında, bunun iki genel aşaması vardır: ayrıştırma ve kod oluşturma .

Ayrıştırma , kodu okuma, onu bir takım resmi kurallara göre yorumlama ve genellikle AST (Özet Sözdizimi Ağacı) olarak bilinen bir ağaç yapısına dönüştürme işlemidir. Programlama dilleri arasındaki tüm çeşitlilik için, bu dikkate değer bir ortaktır: temelde her genel amaçlı programlama dili bir ağaç yapısına ayrıştırılır.

Kod oluşturma , ayrıştırıcının AST'sini girdisi olarak alır ve resmi kuralların uygulanması yoluyla çalıştırılabilir koda dönüştürür. Performans açısından bakıldığında, bu çok daha basit bir iştir; birçok üst seviye dil derleyicisi zamanlarının% 75'ini veya daha fazlasını ayrıştırmaya harcıyor.

Lisp hakkında hatırlanması gereken şey , çok çok eski olması.. Programlama dilleri arasında sadece FORTRAN Lisp'ten daha eskidir. Geçmişe dönersek, ayrıştırma (derlemenin yavaş kısmı) karanlık ve gizemli bir sanat olarak kabul edildi. John McCarthy'nin Lisp teorisi hakkındaki orijinal makaleleri (gerçek bir bilgisayar programlama dili olarak asla gerçekleşmediğini düşünen bir fikir olmasına rağmen), her şey için her yerde modern S ifadelerinden biraz daha karmaşık ve anlamlı bir sözdizimi açıklar. "gösterimde. Bu daha sonra, insanlar gerçekten uygulamaya çalıştığında ortaya çıktı. O zamanlar ayrıştırma işlemi o kadar iyi anlaşılmadığı için, temelde üstüne düştüler ve ayrıştırıcıyı tamamen önemsiz hale getirmek için sözdizimini homoikonik bir ağaç yapısına bıraktılar. Sonuç, sizin (geliştiricinin) ayrıştırıcının çoğunu yapmanız gerektiğidir. Bunun için resmi AST'yi doğrudan kodunuza yazarak çalışın. Homoconicity, diğer her şeyi bu kadar zorlaştıracak kadar "makroları bu kadar kolaylaştırmaz"!

Bununla ilgili sorun, özellikle dinamik yazı yazarken, S ifadelerinin etraflarında çok fazla anlamsal bilgi taşıması çok zor. Tüm sözdiziminiz aynı tür bir şey olduğunda (liste listeleri), sözdizimi tarafından sağlanan bağlam biçiminde pek bir şey yoktur ve bu yüzden makro sistemin çalışacağı çok az şey vardır.

Derleyici teorisi, Lisp'in icat edildiği 1960'lı yıllardan bu yana uzun bir yol kat etti ve başardığı şeyler onun günü için etkileyici olsa da, şimdi oldukça ilkel görünüyorlar. Modern bir metaprogramlama sistemi örneği için (ne yazık ki takdir edilmeyen) Boo diline bir bakın. Boo, statik olarak yazılır, nesne yönelimli ve açık kaynaktır, böylece her AST düğümü, bir makro geliştiricinin kodu okuyabileceği iyi tanımlanmış bir yapıya sahip bir türe sahiptir. Dil, Python'dan esinlenilmiş basit bir sözdizimine sahiptir, bunlardan oluşturulan ağaç yapılarına anlamsal anlam kazandıran çeşitli anahtar kelimeler içerir ve metaprogramlaması, yeni AST düğümlerinin oluşturulmasını kolaylaştırmak için sezgisel bir quasiquote sözdizimine sahiptir.

İşte ben çağırır GUI kodu içinde farklı yerlerde bir grup aynı deseni uygulayarak anlayınca dün oluşturulan bir makro ben var BeginUpdate()bir UI denetimi, bir de bir güncelleme gerçekleştirmek trybloğu ve sonra çağrı EndUpdate():

macro UIUpdate(value as Expression):
    return [|
        $value.BeginUpdate()
        try:
            $(UIUpdate.Body)
        ensure:
            $value.EndUpdate()
    |]

macroKomut, aslında, bir makro kendisi , girdi olarak bir makro gövde alır ve makro işlemek için bir sınıfı oluşturur biri. Makro adını MacroStatementtemsil eden AST düğümü için duran bir değişken olarak makro adını kullanır . [| ... |] bir quasiquote bloğudur, quasiquote bloğunun içindeki ve içindeki koda karşılık gelen AST'yi oluşturur, $ sembolü belirtildiği gibi bir düğümde yer alan "unquote" özelliğini sağlar.

Bununla yazmak mümkündür:

UIUpdate myComboBox:
   LoadDataInto(myComboBox)
   myComboBox.SelectedIndex = 0

ve genişletmek için:

myComboBox.BeginUpdate()
try:
   LoadDataInto(myComboBox)
   myComboBox.SelectedIndex = 0
ensure:
   myComboBox.EndUpdate()

Makroyu bu şekilde ifade etmek, bir Lisp makrosunda olduğundan daha basit ve sezgiseldir, çünkü geliştirici yapının ve özelliklerin MacroStatementnasıl çalıştığını Argumentsve nasıl çalıştığını bilir Bodyve içsel bilginin çok sezgisel olanları ifade eden kavramları ifade etmek için kullanılabileceğini bilir. yol. Aynı zamanda daha güvenlidir, çünkü derleyici yapısını bilir ve bir MacroStatementşey için geçerli olmayan bir şeyi kodlamaya çalışırsanız MacroStatement, derleyici derhal yakalayacaktır ve sizi havaya uçuruncaya kadar bilmemek yerine hatayı rapor edecektir. Çalışma zamanı.

Makroları Haskell, Python, Java, Scala vb. Üzerine aşılamak zor değildir, çünkü bu diller homoikon değildir; bu zor çünkü diller onlar için tasarlanmamış ve dilinizin AST hiyerarşisi bir makro sistem tarafından incelenmek ve manipüle edilmek üzere sıfırdan tasarlandığında en iyi şekilde çalışıyor. Baştan beri metaprogramming ile tasarlanmış bir dille çalışırken, makrolar üzerinde çalışmak çok daha basit ve daha kolaydır!


4
Sevinçle okumak, teşekkür ederim! Lisp olmayan makrolar, sözdizimini değiştirmek kadar uzar mı? Çünkü Lisp’in gücünden biri sözdiziminin hepsi aynı olduğundan, her ikisi de aynı olduğu için bir fonksiyon, koşullu ifade eklemek kolaydır. Lisp dili olmayan dillerle bir şey diğerinden farklı olsa da if..., örneğin bir işlev çağrısı gibi görünmüyor. Boo'yu tanımıyorum ama Boo'nun desen eşleştirmesi olmadığını hayal edin, makroyu kendi sözdizimiyle tanıtır mısınız? Demek istediğim - Lisp'teki herhangi bir yeni makro, çalıştığı diğer dillerde% 100 doğal hissediyor, ancak dikişleri görebilirsiniz.
greenoldman

4
Her zaman okuduğum gibi hikaye biraz farklı. S-ifadesine alternatif bir sözdizimi planlandı, ancak bunun üzerinde çalışmak gecikti çünkü programcılar zaten s-ifadelerini kullanmaya başlamış ve bunları uygun bulmuşlardı. Böylece yeni sözdizimi üzerindeki çalışmalar sonunda unutuldu. Derleyici teorisinin eksikliklerini s-ifadelerini kullanmanın nedeni olarak gösteren kaynağı belirtebilir misiniz? Ayrıca, Lisp ailesi yıllarca gelişmeye devam etti (Şema, Ortak Lisp, Clojure) ve çoğu lehçe s-ifadelerine uymaya karar verdi.
Giorgio

5
"daha basit ve daha sezgisel": üzgünüm, ama nasıl göremiyorum. "Updating.Arguments [0]" dır değil anlamlı, oldukça adlandırılmış argüman var ve derleyici onay itselfs izin verseydin argümanlar maç sayısı: pastebin.com/YtUf1FpG
coredump

8
"Performans açısından bu çok daha basit bir iştir; birçok üst seviye dil derleyicisi zamanlarının% 75'ini veya daha fazlasını ayrıştırmaya harcar." Çoğu zaman almak için optimizasyonlar aramayı ve uygulamalarını beklerdim (ama hiçbir zaman gerçek bir derleyici yazmadım ). Burada bir şey mi eksik?
Doval

5
Maalesef, örneğiniz bunu göstermiyor. Makro içeren herhangi bir Lisp'te uygulamak ilkeldir. Aslında bu, uygulanacak en ilkel makrolardan biridir. Bu, Lisp'teki makrolar hakkında fazla bir şey bilmediğinizden şüphelenmemi sağlıyor. “Lisp'in sözdizimi 1960'larda kaldı”: aslında Lisp'teki makro sistemler 1960'tan beri çok ilerleme kaydetti (1960'da Lisp'in makroları bile yoktu!).
Rainer Joswig

3

SICP'den Şema öğreniyorum ve Scheme'i yapan şeyin büyük bir kısmının ve daha özel olarak LISP'in makro sistem olduğu izlenimini alıyorum.

Nasıl yani? SICP'deki tüm kodlar makro tarzında yazılmıştır. SICP'de makro yok. Sadece sayfa 373'deki dipnotta makrolardan bahsedilmiştir.

Ancak, makrolar derleme zamanında genişletildiğinden

Mutlaka gerekli değiller. Lisp, tercüman ve derleyicilerde makrolar sağlar. Dolayısıyla bir derleme zamanı olmayabilir. Lisp tercümanınız varsa, makrolar yürütme sırasında genişletilir. Birçok Lisp sisteminde yerleşik bir derleyici bulunduğundan, kod çalıştırılabilir ve çalışma zamanında derlenebilir.

Bunu bir Common Lisp uygulaması olan SBCL kullanarak test edelim.

SBCL'yi tercümana değiştirelim:

* (setf sb-ext:*evaluator-mode* :interpret)

:INTERPRET

Şimdi bir makro tanımlarız. Makro genişletilmiş kod çağrıldığında bir şey yazdırır. Üretilen kod yazdırılmaz.

* (defmacro my-and (a b)
    (print "macro my-and used")
    `(if ,a
         (if ,b t nil)
         nil))

Şimdi makroyu kullanalım:

MY-AND
* (defun foo (a b) (my-and a b))

FOO

Görmek. Yukarıdaki durumda Lisp hiçbir şey yapmaz. Makro tanım zamanında genişletilmez.

* (foo t nil)

"macro my-and used"
NIL

Ancak çalışma zamanında, kod kullanıldığında, makro genişletilir.

* (foo t t)

"macro my-and used"
T

Yine, çalışma zamanında, kod kullanıldığında, makro genişletilir.

Derleyici kullanıldığında SBCL'nin yalnızca bir kez genişleyeceğini unutmayın. Ancak çeşitli Lisp uygulamaları aynı zamanda SBCL gibi tercümanlar da sağlamaktadır.

Lisp'te makrolar neden kolaydır? Gerçekten kolay değiller. Yalnızca Lisps'te yerleşik makro desteği olan pek çok kişi var. Birçok Lisps makro için kapsamlı makinelerle geldiğinden, kolay gibi görünüyor. Ancak makro mekanizmalar oldukça karmaşık olabilir.


SICP'yi okumanın yanı sıra web'de Scheme hakkında çok şey okudum. Ayrıca, Lisp ifadeleri yorumlanmadan önce derlenmedi mi? En azından ayrıştırılmalıdırlar. Yani "derleme zamanı", "ayrıştırma zamanı" olmalı.
Elliot Gorokhovsky

@ RenéG Rainer'in amacı, siz evalya da loadherhangi bir Lisp dilinde kod yazmanız durumunda , bunlardaki makroların da işleneceği yönündedir . Oysa sorunuzda önerildiği gibi bir önişlemci sistemi kullanıyorsanız evalve benzerleri makro genişlemesinden fayda görmez.
Chris Jester-Young,

@ RenéG Ayrıca, "ayrıştırma" readLisp'te de denir . Bu ayrım önemlidir, çünkü evalmetin biçiminde değil, liste veri yapıları (benim cevabımda belirtildiği gibi) üzerinde çalışır. Böylece (eval '(+ 1 1))2'yi kullanabilir ve geri alabilirsiniz, ancak eğer (eval "(+ 1 1)")öyleyseniz geri dönersiniz "(+ 1 1)"(sicim). Kullanılacak readalmak "(+ 1 1)"(7 karakter dizesi) için (+ 1 1)(bir sembol ve iki fixnums ile bir liste).
Chris Jester-Young,

@ RenéG Bu anlayışla makrolar her zaman çalışmaz read. Derleme zamanında çalışırlar; kodunuz varsa , kod çalıştırıldığında, kod çalıştırıldığında, kod çalıştırıldığında (kod çalıştırıldığında) yalnızca bir kez (Şema'da) (and (test1) (test2))genişleyecektir; , çalışma zamanında bu ifadeyi uygun şekilde derler (ve makro genişletir). (if (test1) (test2) #f)(eval '(and (test1) (test2)))
Chris Jester-Young,

@ RenéG Homoiconicity, Lisp dillerinin metinsel biçim yerine liste yapılarını ve bu yapıların yürütmeden önce dönüştürülmelerini (makrolar aracılığıyla) değerlendirmesini sağlayan şeydir. Çoğu dil evalyalnızca metin dizelerinde çalışır ve sözdizimi değişikliği için yetenekleri çok daha yumuşak ve / veya hantaldır.
Chris Jester-Young,

1

Homoconicity , makroları uygulamayı çok daha kolaylaştırır. Kodun veri ve veri olduğu düşüncesi koddur, az veya çok sayıda (tanımlayıcıların yanlışlıkla yakalanması, hijyenik makrolar ile çözülmesi ), birinin diğerinin serbestçe ikame edilmesi mümkün değildir. Lisp ve Scheme, düzgün bir şekilde yapılandırılmış olan S ifadelerinin sözdizimi ile kolaylaşır ve böylece Syntactic Macros'un temelini oluşturan AST'lere dönüşmesi kolaydır .

S-İfadeleri veya Homoconicity'siz diller, yine de kesinlikle yapılabilmesine rağmen, Syntactic Macros'u uygulamakta zorlanacaktır. Proje Kepler , örneğin onları Scala ile tanıştırmaya çalışıyor.

Homoconicity dışında bir sözdizimi makroları kullanımı ile en büyük sorun, keyfi olarak oluşturulan sözdizimi sorunudur. Muazzam esneklik ve güç sunarlar, ancak kaynak kodunuzun anlaşılması ya da bakımı artık bu kadar kolay olmayabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.