Ich lerne Schema von der SICP und habe den Eindruck, dass ein großer Teil dessen, was Schema und vor allem LISP ausmacht, das Makrosystem ist. Aber, da Makros zur Kompilierungszeit erweitert werden, warum stellen die Leute keine äquivalenten Makrosysteme für C / Python / Java / whatever her? Zum Beispiel könnte man den python
Befehl an expand-macros | python
oder was auch immer binden . Der Code ist für Benutzer, die das Makrosystem nicht verwenden, weiterhin portierbar. Sie müssen lediglich die Makros erweitern, bevor Sie den Code veröffentlichen. Aber ich kenne so etwas nicht, außer Vorlagen in C ++ / Haskell, von denen ich erfahre, dass sie nicht wirklich gleich sind. Was ist mit LISP, wenn überhaupt, das Implementieren von Makrosystemen erleichtert?
21
Antworten:
Viele Lispers werden Ihnen sagen, dass das Besondere an Lisp die Homoikonizität ist , was bedeutet, dass die Syntax des Codes unter Verwendung derselben Datenstrukturen wie bei anderen Daten dargestellt wird. Hier ist zum Beispiel eine einfache Funktion (unter Verwendung der Schemasyntax) zum Berechnen der Hypotenuse eines rechtwinkligen Dreiecks mit den angegebenen Seitenlängen:
Homoikonizität besagt nun, dass der obige Code tatsächlich als Datenstruktur (insbesondere Listen von Listen) in Lisp-Code darstellbar ist. Betrachten Sie daher die folgenden Listen und sehen Sie, wie sie zusammenkleben:
(define #2# #3#)
(hypot x y)
(sqrt #4#)
(+ #5# #6#)
(square x)
(square y)
Mit Makros können Sie den Quellcode wie folgt behandeln: Listen von Dingen. Jede dieser 6 „Teil - Listen“ enthält entweder Verweise auf andere Listen oder auf Symbole (in diesem Beispiel:
define
,hypot
,x
,y
,sqrt
,+
,square
).Wie können wir also Homoikonizität nutzen, um die Syntax auseinanderzunehmen und Makros zu erstellen? Hier ist ein einfaches Beispiel. Lassen Sie uns das
let
Makro neu implementieren , das wir aufrufenmy-let
. Als eine Erinnerung,sollte in erweitern
Hier ist eine Implementierung Scheme „explizite Umbenennung“ Makros † :
Der
form
Parameter ist an die tatsächliche Form gebunden, in unserem Beispiel also(my-let ((foo 1) (bar 2)) (+ foo bar))
. Lasst uns also das Beispiel durcharbeiten:cadr
schnappt sich den((foo 1) (bar 2))
Teil des Formulars.Dann holen wir den Körper aus der Form.
cddr
schnappt sich den((+ foo bar))
Teil des Formulars. (Beachten Sie, dass dies dazu gedacht ist, alle Unterformulare nach dem Binden zu erfassendann wäre der Körper
((debug foo) (debug bar) (+ foo bar))
.)lambda
Ausdruck und rufen ihn mit den von uns gesammelten Bindings und Body auf. Der Backtick wird als "Quasiquot" bezeichnet. Dies bedeutet, dass alles innerhalb des Quasiquots als wörtliche Bezugspunkte behandelt wird, mit Ausnahme der Nachkommastellen ("unquote").(rename 'lambda)
Möglichkeit, die bei der Definitionlambda
dieses Makros gültige Bindung zu verwenden , und nicht die Bindung, die bei der Verwendung dieses Makros auftreten kann . (Dies wird als Hygiene bezeichnet .)lambda
(map car bindings)
gibt zurück(foo bar)
: das erste Datum in jeder der Bindungen.(map cadr bindings)
gibt zurück(1 2)
: das zweite Datum in jeder der Bindungen.,@
Tut "Splicing", das für Ausdrücke verwendet wird, die eine Liste zurückgeben: Es bewirkt, dass die Elemente der Liste in das Ergebnis und nicht in die Liste selbst eingefügt werden.(($lambda (foo bar) (+ foo bar)) 1 2)
, in der$lambda
hier auf die umbenannte verwiesen wirdlambda
.Unkompliziert, richtig? ;-) (Wenn es für Sie nicht einfach ist, stellen Sie sich vor, wie schwierig es wäre, ein Makrosystem für andere Sprachen zu implementieren.)
Sie können also über Makrosysteme für andere Sprachen verfügen, wenn Sie in der Lage sind, den Quellcode auf unkomplizierte Weise zu "trennen". Es gibt einige Versuche dazu. Zum Beispiel macht sweet.js dies für JavaScript.
† Für erfahrene Schemas, die dies lesen, habe ich absichtlich explizite Umbenennungsmakros als mittleren Kompromiss zwischen
defmacro
s verwendet, die von anderen Lisp-Dialekten verwendet werden, undsyntax-rules
(dies wäre die Standardmethode, um ein solches Makro in Scheme zu implementieren). Ich möchte nicht in anderen Lisp-Dialekten schreiben, aber ich möchte keine Nicht-Schemas entfremden, die es nicht gewohnt sindsyntax-rules
.Als Referenz ist hier das
my-let
Makro, das verwendetsyntax-rules
:Die entsprechende
syntax-case
Version sieht sehr ähnlich aus:Der Unterschied zwischen den beiden besteht darin, dass auf alles in
syntax-rules
ein implizites#'
angewendet wird, sodass Sie nur Muster- / Vorlagenpaare insyntax-rules
diesem Element haben können. Daher ist es vollständig deklarativ. Im Gegensatz dazu ist insyntax-case
das Bit nach dem Muster tatsächlicher Code, der am Ende ein Syntaxobjekt (#'(...)
) zurückgeben muss, aber auch anderen Code enthalten kann.quelle
syntax-rules
, was rein deklarativ ist. Für komplizierte Makros kann ich verwendensyntax-case
, was teilweise deklarativ und teilweise prozedural ist. Und dann gibt es eine explizite Umbenennung, die rein prozedural ist. (Die meisten Scheme-Implementierungen bieten entwedersyntax-case
oder ER. Ich habe noch keine gesehen, die beides bietet. Sie haben eine äquivalente Leistung.)Eine abweichende Meinung: Lisps Homoikonizität ist weitaus weniger nützlich, als die meisten Lisp-Fans glauben machen würden.
Um syntaktische Makros zu verstehen, ist es wichtig, Compiler zu verstehen. Die Aufgabe eines Compilers besteht darin, von Menschen lesbaren Code in ausführbaren Code umzuwandeln. Aus einer sehr allgemeinen Perspektive besteht dies aus zwei Phasen: Analyse und Codegenerierung .
Beim Parsen wird Code gelesen, gemäß einer Reihe von formalen Regeln interpretiert und in eine Baumstruktur umgewandelt, die allgemein als AST (Abstract Syntax Tree) bezeichnet wird. Bei aller Vielfalt der Programmiersprachen ist dies eine bemerkenswerte Gemeinsamkeit: Im Wesentlichen wird jede universelle Programmiersprache in einer Baumstruktur analysiert.
Bei der Codegenerierung wird der AST des Parsers als Eingabe verwendet und durch Anwendung formaler Regeln in ausführbaren Code umgewandelt. Aus Sicht der Leistung ist dies eine viel einfachere Aufgabe. Viele hochrangige Sprachkompilierer verbringen 75% oder mehr ihrer Zeit mit dem Parsen.
Das Besondere an Lisp ist, dass es sehr, sehr alt ist. Unter den Programmiersprachen ist nur FORTRAN älter als Lisp. Vor langer Zeit galt das Parsen (der langsame Teil des Kompilierens) als dunkle und mysteriöse Kunst. John McCarthys Originalarbeiten über die Theorie von Lisp (damals war es nur eine Idee, von der er nie gedacht hatte, dass sie tatsächlich als echte Computerprogrammiersprache implementiert werden könnten) beschreiben eine etwas komplexere und ausdrucksstärkere Syntax als die modernen "S-Ausdrücke überall für alles" "notation. Das kam später, als die Leute versuchten, es tatsächlich umzusetzen. Da das Parsen damals nicht gut verstanden wurde, haben sie es im Grunde genommen auf den Kopf gestellt und die Syntax in eine homoikonische Baumstruktur getaucht, um die Arbeit des Parsers äußerst trivial zu machen. Das Endergebnis ist, dass Sie (der Entwickler) viel Parser machen müssen. ' s arbeiten dafür, indem Sie das formale AST direkt in Ihren Code schreiben. Homoikonizität "macht Makros nicht so viel einfacher", sondern macht das Schreiben von allem anderen noch viel schwieriger!
Das Problem dabei ist, dass es für S-Ausdrücke, insbesondere bei dynamischer Typisierung, sehr schwierig ist, viele semantische Informationen mit sich herumzutragen. Wenn alle Ihre Syntax dieselbe Art von Dingen (Listen von Listen) ist, gibt es nicht viel in der Art von Kontext, die durch die Syntax bereitgestellt wird, und daher hat das Makrosystem sehr wenig zu arbeiten.
Die Compilertheorie hat seit den 1960er Jahren, als Lisp erfunden wurde, einen langen Weg zurückgelegt, und obwohl die von ihr geleisteten Arbeiten für seine Zeit beeindruckend waren, sehen sie jetzt eher primitiv aus. Schauen Sie sich als Beispiel für ein modernes Metaprogrammiersystem die (leider unterschätzte) Boo-Sprache an. Boo ist statisch typisiert, objektorientiert und Open Source, sodass jeder AST-Knoten einen Typ mit einer genau definierten Struktur hat, in den ein Makroentwickler den Code einlesen kann. Die Sprache hat eine relativ einfache Syntax, die von Python inspiriert ist, mit verschiedenen Schlüsselwörtern, die den darauf aufbauenden Baumstrukturen eine eigene semantische Bedeutung verleihen, und ihre Metaprogrammierung hat eine intuitive Quasiquot-Syntax, um die Erstellung neuer AST-Knoten zu vereinfachen.
Hier ist ein Makro, das ich gestern erstellt habe, als ich feststellte, dass ich dasselbe Muster auf eine Reihe von verschiedenen Stellen im GUI-Code angewendet habe, wo ich
BeginUpdate()
ein UI-Steuerelement aufrief, eine Aktualisierung in einemtry
Block durchführte und dann aufriefEndUpdate()
:Der
macro
Befehl ist in der Tat ein Makro selbst , das einen Makrotext als Eingabe verwendet und eine Klasse zur Verarbeitung des Makros generiert. Der Name des Makros wird als Variable verwendet, die für denMacroStatement
AST-Knoten steht, der den Makroaufruf darstellt. Die [| ... |] ist ein Quasiquote-Block, der den AST generiert, der dem Code innerhalb des Quasiquote-Blocks entspricht, und innerhalb des Quasiquote-Blocks stellt das $ -Symbol die Funktion "unquote" bereit, die in einem Knoten wie angegeben ersetzt wird.Damit ist es möglich zu schreiben:
und lassen Sie es erweitern auf:
Ausdruck den Makro diese Art und Weise ist einfacher und intuitiver als es in einem Lisp Makro sein würde, da der Entwickler , die die Struktur kennt
MacroStatement
und weiß , wie dieArguments
undBody
Eigenschaften der Arbeit, und das inhärente Wissen kann verwendet werden , um die Konzepte in einem sehr intuitiven beteiligt auszudrücken Weg. Es ist auch sicherer, weil der Compiler die Struktur von kenntMacroStatement
und wenn Sie versuchen, etwas zu codieren, das für a nicht gültig istMacroStatement
, wird der Compiler es sofort abfangen und den Fehler melden, anstatt dass Sie es nicht wissen, bis etwas bei Ihnen explodiert Laufzeit.Das Übertragen von Makros auf Haskell, Python, Java, Scala usw. ist nicht schwierig, da diese Sprachen nicht homoikonisch sind. Dies ist schwierig, da die Sprachen nicht für sie entwickelt wurden. Dies funktioniert am besten, wenn die AST-Hierarchie Ihrer Sprache von Grund auf so entwickelt wurde, dass sie von einem Makrosystem überprüft und bearbeitet werden kann. Wenn Sie mit einer Sprache arbeiten, die von Anfang an unter Berücksichtigung der Metaprogrammierung entwickelt wurde, sind Makros viel einfacher und einfacher zu handhaben!
quelle
if...
sieht es beispielsweise nicht wie ein Funktionsaufruf aus. Ich kenne Boo nicht, aber stelle dir vor, Boo hätte keine Mustererkennung. Könnten Sie es mit einer eigenen Syntax als Makro einführen? Mein Punkt ist - jedes neue Makro in Lisp fühlt sich 100% natürlich an, in anderen Sprachen funktionieren sie, aber Sie können die Stiche sehen.Wieso das? Der gesamte Code in SICP ist makrofrei geschrieben. In SICP gibt es keine Makros. Nur in einer Fußnote auf Seite 373 werden Makros jemals erwähnt.
Sie sind nicht unbedingt. Lisp stellt Makros sowohl für Interpreter als auch für Compiler bereit. Daher gibt es möglicherweise keine Kompilierungszeit. Wenn Sie einen Lisp-Interpreter haben, werden Makros zur Ausführungszeit erweitert. Da viele Lisp-Systeme über einen Compiler verfügen, kann Code generiert und zur Laufzeit kompiliert werden.
Testen wir das mit SBCL, einer Common Lisp-Implementierung.
Lassen Sie uns SBCL auf den Interpreter umstellen:
Nun definieren wir ein Makro. Das Makro druckt etwas, wenn es für Code erweitert aufgerufen wird. Der generierte Code wird nicht gedruckt.
Verwenden wir nun das Makro:
Sehen. In obigem Fall tut Lisp nichts. Das Makro wird zum Definitionszeitpunkt nicht erweitert.
Zur Laufzeit wird das Makro jedoch erweitert, wenn der Code verwendet wird.
Wiederum wird zur Laufzeit, wenn der Code verwendet wird, das Makro erweitert.
Beachten Sie, dass SBCL bei Verwendung eines Compilers nur einmal erweitert wird. Verschiedene Lisp-Implementierungen bieten aber auch Interpreten - wie SBCL.
Warum sind Makros in Lisp einfach? Nun, sie sind nicht wirklich einfach. Nur in Lisps, und es gibt viele, die eine eingebaute Makrounterstützung haben. Da viele Lisps über umfangreiche Maschinen für Makros verfügen, sieht es so aus, als ob es einfach ist. Makromechanismen können jedoch äußerst kompliziert sein.
quelle
eval
oderload
Code in einer beliebigen Lisp-Sprache, Makros in diesen auch verarbeitet werden. Wenn Sie hingegen ein Präprozessorsystem verwenden, wie in Ihrer Frage vorgeschlagen, wirdeval
die Makroerweiterung keine Vorteile für Sie haben.read
in Lisp. Diese Unterscheidung ist wichtig, daeval
die Listendatenstrukturen (wie in meiner Antwort erwähnt) tatsächlich bearbeitet werden und nicht die Textform. Du kannst also 2 benutzen(eval '(+ 1 1))
und zurückholen, aber wenn du(eval "(+ 1 1)")
, erhalten Sie zurück"(+ 1 1)"
(die Zeichenfolge). Sie verwendenread
, um von"(+ 1 1)"
(eine Zeichenfolge von 7 Zeichen) zu(+ 1 1)
(eine Liste mit einem Symbol und zwei Fixnummern) zu gelangen.read
gleichen Zeit. Sie funktionieren zur Kompilierungszeit in dem Sinne, dass, wenn Sie Code wie haben(and (test1) (test2))
, dieser(if (test1) (test2) #f)
nur einmal erweitert wird, wenn der Code geladen wird, und nicht jedes Mal, wenn der Code ausgeführt wird, aber wenn Sie so etwas tun(eval '(and (test1) (test2)))
, das diesen Ausdruck zur Laufzeit entsprechend kompiliert (und makro-expandiert).eval
arbeiten nur mit Textzeichenfolgen, und ihre Möglichkeiten zur Syntaxänderung sind weitaus weniger ausgeprägt und / oder umständlicher.Homoiconicity erleichtert die Implementierung von Makros erheblich. Die Idee, dass Code Daten und Daten Code sind, macht es möglich, mehr oder weniger (außer bei versehentlicher Erfassung von Identifikatoren, die durch hygienische Makros gelöst werden ) frei voneinander zu ersetzen. Lisp und Scheme vereinfachen dies durch ihre Syntax von S-Ausdrücken, die einheitlich strukturiert sind und sich daher leicht in ASTs umwandeln lassen, die die Grundlage für syntaktische Makros bilden .
Sprachen ohne S-Ausdrücke oder Homoikonizität werden Probleme haben, Syntaktische Makros zu implementieren, obwohl dies mit Sicherheit noch möglich ist. Das Projekt Kepler versucht, sie beispielsweise in Scala einzuführen.
Das größte Problem bei der Verwendung von Syntaxmakros ist neben der Nichthomoikonizität das Problem der willkürlich generierten Syntax. Sie bieten enorme Flexibilität und Leistung, aber zu dem Preis, den Ihr Quellcode möglicherweise nicht mehr so einfach zu verstehen oder zu warten ist.
quelle