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 einem try
Block durchführte und dann aufrief EndUpdate()
:
macro UIUpdate(value as Expression):
return [|
$value.BeginUpdate()
try:
$(UIUpdate.Body)
ensure:
$value.EndUpdate()
|]
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 den MacroStatement
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:
UIUpdate myComboBox:
LoadDataInto(myComboBox)
myComboBox.SelectedIndex = 0
und lassen Sie es erweitern auf:
myComboBox.BeginUpdate()
try:
LoadDataInto(myComboBox)
myComboBox.SelectedIndex = 0
ensure:
myComboBox.EndUpdate()
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 die Arguments
und Body
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 kennt MacroStatement
und wenn Sie versuchen, etwas zu codieren, das für a nicht gültig ist MacroStatement
, 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!