Was macht Lisp-Makros so besonders?

297

Wenn man Paul Grahams Essays über Programmiersprachen liest, würde man denken, dass Lisp-Makros der einzige Weg sind. Als vielbeschäftigter Entwickler, der auf anderen Plattformen arbeitet, hatte ich nicht das Privileg, Lisp-Makros zu verwenden. Erklären Sie als jemand, der die Begeisterung verstehen möchte, was diese Funktion so leistungsfähig macht.

Bitte beziehen Sie dies auch auf etwas, das ich aus den Welten der Python-, Java-, C # - oder C-Entwicklung verstehen würde.

Minze
quelle
2
Übrigens gibt es einen LISP-ähnlichen Makroprozessor für C # namens LeMP: ecsharp.net/lemp ... JavaScript hat auch einen namens Sweet.js: sweetjs.org
Qwertie
@Qwertie Funktioniert sweetjs heutzutage überhaupt?
fredrik.hjarner
Ich habe es nicht benutzt, aber das letzte Commit war vor sechs Monaten ... gut genug für mich!
Qwertie

Antworten:

308

Um die kurze Antwort zu geben, werden Makros zum Definieren von Sprachsyntaxerweiterungen für Common Lisp oder Domain Specific Languages ​​(DSLs) verwendet. Diese Sprachen sind direkt in den vorhandenen Lisp-Code eingebettet. Jetzt können die DSLs eine ähnliche Syntax wie Lisp (wie Peter Norvigs Prolog Interpreter für Common Lisp) oder eine völlig andere (z. B. Infix Notation Math für Clojure) haben.

Hier ist ein konkreteres Beispiel: In
Python sind Listenverständnisse in die Sprache integriert. Dies gibt eine einfache Syntax für einen häufigen Fall. Die Linie

divisibleByTwo = [x for x in range(10) if x % 2 == 0]

liefert eine Liste mit allen geraden Zahlen zwischen 0 und 9. In den 1,5 Tagen von Python gab es keine solche Syntax. Sie würden so etwas eher verwenden:

divisibleByTwo = []
for x in range( 10 ):
   if x % 2 == 0:
      divisibleByTwo.append( x )

Diese sind beide funktional gleichwertig. Lassen Sie uns unsere Aufhebung des Unglaubens aufrufen und so tun, als hätte Lisp ein sehr begrenztes Schleifenmakro, das nur Iteration ausführt und keine einfache Möglichkeit bietet, das Äquivalent von Listenverständnissen zu erreichen.

In Lisp könnte man folgendes schreiben. Ich sollte beachten, dass dieses erfundene Beispiel so ausgewählt ist, dass es mit dem Python-Code identisch ist, kein gutes Beispiel für Lisp-Code.

;; the following two functions just make equivalent of Python's range function
;; you can safely ignore them unless you are running this code
(defun range-helper (x)
  (if (= x 0)
      (list x)
      (cons x (range-helper (- x 1)))))

(defun range (x)
  (reverse (range-helper (- x 1))))

;; equivalent to the python example:
;; define a variable
(defvar divisibleByTwo nil)

;; loop from 0 upto and including 9
(loop for x in (range 10)
   ;; test for divisibility by two
   if (= (mod x 2) 0) 
   ;; append to the list
   do (setq divisibleByTwo (append divisibleByTwo (list x))))

Bevor ich weiter gehe, sollte ich besser erklären, was ein Makro ist. Es ist eine Transformation, die Code für Code ausgeführt wird. Das heißt, ein Code, der vom Interpreter (oder Compiler) gelesen wird und Code als Argument verwendet, manipuliert und gibt das Ergebnis zurück, das dann direkt ausgeführt wird.

Das ist natürlich viel Tippen und Programmierer sind faul. So könnten wir DSL für das Listenverständnis definieren. Tatsächlich verwenden wir bereits ein Makro (das Schleifenmakro).

Lisp definiert einige spezielle Syntaxformen. Das Zitat ( ') gibt an, dass das nächste Token ein Literal ist. Das Quasiquote oder Backtick ( `) zeigt an, dass das nächste Token ein Literal mit Escapezeichen ist. Escapezeichen werden vom Komma-Operator angezeigt. Das Literal '(1 2 3)entspricht dem von Python [1, 2, 3]. Sie können es einer anderen Variablen zuweisen oder an Ort und Stelle verwenden. Sie können sich `(1 2 ,x)das Äquivalent zu Pythons vorstellen, [1, 2, x]bei dem xes sich um eine zuvor definierte Variable handelt. Diese Listennotation ist Teil der Magie, die in Makros steckt. Der zweite Teil ist der Lisp-Reader, der den Code auf intelligente Weise durch Makros ersetzt.

So können wir ein Makro namens lcomp(kurz für Listenverständnis) definieren. Die Syntax entspricht genau der Python, die wir im Beispiel verwendet haben [x for x in range(10) if x % 2 == 0] -(lcomp x for x in (range 10) if (= (% x 2) 0))

(defmacro lcomp (expression for var in list conditional conditional-test)
  ;; create a unique variable name for the result
  (let ((result (gensym)))
    ;; the arguments are really code so we can substitute them 
    ;; store nil in the unique variable name generated above
    `(let ((,result nil))
       ;; var is a variable name
       ;; list is the list literal we are suppose to iterate over
       (loop for ,var in ,list
            ;; conditional is if or unless
            ;; conditional-test is (= (mod x 2) 0) in our examples
            ,conditional ,conditional-test
            ;; and this is the action from the earlier lisp example
            ;; result = result + [x] in python
            do (setq ,result (append ,result (list ,expression))))
           ;; return the result 
       ,result)))

Jetzt können wir über die Kommandozeile ausführen:

CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0))
(0 2 4 6 8)

Ziemlich ordentlich, oder? Jetzt hört es hier nicht auf. Sie haben einen Mechanismus oder einen Pinsel, wenn Sie möchten. Sie können jede gewünschte Syntax verwenden. Wie die withSyntax von Python oder C # . Oder die LINQ-Syntax von .NET. Dies ist es, was die Menschen zu Lisp anzieht - ultimative Flexibilität.

gte525u
quelle
51
+1 für die Implementierung des Listenverständnisses in Lisp, denn warum nicht?
ckb
9
@ckb Tatsächlich hat LISP bereits ein Listenverständnis-Makro in der Standardbibliothek : (loop for x from 0 below 10 when (evenp x) collect x), weitere Beispiele hier . Aber in der Tat ist Schleife "nur ein Makro" (ich habe es vor einiger Zeit von Grund auf neu implementiert )
Suzanne Dupéron
8
Ich weiß, dass es ziemlich unabhängig ist, aber ich frage mich über die Syntax und wie das Parsen tatsächlich funktioniert ... Angenommen, ich rufe lcomp auf diese Weise auf (Ändern des Thirs-Elements von "für" in "azertyuiop"): (lcomp x azertyuiop x in ( Bereich 10) Wenn (= (% x 2) 0)) funktioniert das Makro weiterhin wie erwartet? Oder wird der Parameter "for" in der Schleife verwendet, sodass er beim Aufruf die Zeichenfolge "for" sein muss?
Dader
2
@dader Die Schleife ist also tatsächlich ein anderes Makro und für ist ein spezielles Symbol, das in diesem Makro verwendet wird. Ich hätte das Makro ohne das Schleifenmakro leicht definieren können. Wenn ich es getan hätte, wäre der Beitrag jedoch viel länger gewesen. Das Schleifenmakro und seine gesamte Syntax sind in CLtL definiert .
gte525u
3
Eine Sache, die mich mit den Makros anderer Sprachen verwechselt, ist, dass ihre Makros durch die Syntax der Hostsprache begrenzt sind. Können Lispy-Makros Nicht-Lispy-Syntax interpretieren? Ich meine, stellen Sie sich vor, Sie erstellen eine Hashkell-ähnliche Syntax (keine Paranthese) und interpretieren sie mit Lisp-Makros. Ist dies möglich und welche Vor- und Nachteile hat die Verwendung von Makros im Vergleich zur direkten Verwendung von Lexer und Parser?
CMCDragonkai
105

Eine umfassende Debatte rund um das Lisp-Makro finden Sie hier .

Eine interessante Untergruppe dieses Artikels:

In den meisten Programmiersprachen ist die Syntax komplex. Makros müssen die Programmsyntax zerlegen, analysieren und wieder zusammensetzen. Sie haben keinen Zugriff auf den Parser des Programms, daher müssen sie auf Heuristiken und Best-Guesses angewiesen sein. Manchmal ist ihre Analyse der Senkungsrate falsch, und dann brechen sie.

Aber Lisp ist anders. Lisp - Makros tut Zugriff auf den Parser hat, und es ist ein wirklich einfacher Parser. Einem Lisp-Makro wird keine Zeichenfolge übergeben, sondern ein vorbereiteter Quellcode in Form einer Liste, da die Quelle eines Lisp-Programms keine Zeichenfolge ist. Es ist eine Liste. Und Lisp-Programme sind wirklich gut darin, Listen auseinanderzunehmen und wieder zusammenzusetzen. Sie tun dies jeden Tag zuverlässig.

Hier ist ein erweitertes Beispiel. Lisp hat ein Makro namens "setf", das die Zuweisung ausführt. Die einfachste Form von setf ist

  (setf x whatever)

Hiermit wird der Wert des Symbols "x" auf den Wert des Ausdrucks "was auch immer" gesetzt.

Lisp hat auch Listen; Sie können die Funktionen "car" und "cdr" verwenden, um das erste Element einer Liste bzw. den Rest der Liste abzurufen.

Was ist nun, wenn Sie das erste Element einer Liste durch einen neuen Wert ersetzen möchten? Es gibt eine Standardfunktion dafür, und unglaublich, sein Name ist noch schlimmer als "Auto". Es ist "rplaca". Aber Sie müssen sich nicht an "rplaca" erinnern, weil Sie schreiben können

  (setf (car somelist) whatever)

das Auto von Somelist setzen.

Was hier wirklich passiert ist, dass "setf" ein Makro ist. Zur Kompilierungszeit prüft es seine Argumente und sieht, dass das erste die Form hat (Auto ETWAS). Es sagt sich: "Oh, der Programmierer versucht, das Auto auf etwas einzustellen. Die dafür zu verwendende Funktion ist 'rplaca'." Und es schreibt den Code leise neu, um:

  (rplaca somelist whatever)
VonC
quelle
5
setf ist ein schönes Beispiel für die Leistungsfähigkeit von Makros, danke, dass Sie es aufgenommen haben.
Joel
Ich mag das Highlight, weil die Quelle eines Lisp-Programms kein String ist. Es ist eine Liste. ! Ist es der Hauptgrund, warum das LISP-Makro aufgrund seiner Klammern besser ist als das der meisten anderen?
Student
@Student Ich nehme an: books.google.fr/… schlägt vor, dass Sie Recht haben.
VonC
55

Gängige Lisp-Makros erweitern im Wesentlichen die "syntaktischen Grundelemente" Ihres Codes.

In C funktioniert das switch / case-Konstrukt beispielsweise nur mit integralen Typen. Wenn Sie es für Floats oder Strings verwenden möchten, bleiben verschachtelte if-Anweisungen und explizite Vergleiche übrig. Es gibt auch keine Möglichkeit, ein C-Makro zu schreiben, um die Arbeit für Sie zu erledigen.

Da ein Lisp-Makro (im Wesentlichen) ein Lisp-Programm ist, das Codefragmente als Eingabe verwendet und Code zurückgibt, um den "Aufruf" des Makros zu ersetzen, können Sie Ihr Repertoire an "Primitiven" so weit erweitern, wie Sie möchten mit einem besser lesbaren Programm.

Um dasselbe in C zu tun, müssten Sie einen benutzerdefinierten Vorprozessor schreiben, der Ihre ursprüngliche (nicht ganz C) Quelle frisst und etwas ausspuckt, das ein C-Compiler verstehen kann. Es ist kein falscher Weg, aber es ist nicht unbedingt der einfachste.

Vatine
quelle
41

Mit Lisp-Makros können Sie entscheiden, wann (wenn überhaupt) ein Teil oder Ausdruck ausgewertet wird. Um ein einfaches Beispiel zu nennen, denken Sie an Cs:

expr1 && expr2 && expr3 ...

Was dies sagt, ist: Bewerten expr1und, falls es wahr sein sollte, bewerten expr2usw.

Versuchen Sie nun, daraus &&eine Funktion zu machen ... das ist richtig, das können Sie nicht. So etwas nennen wie:

and(expr1, expr2, expr3)

Wird alle drei bewerten, exprsbevor eine Antwort gegeben wird, unabhängig davon, ob sie expr1falsch war!

Mit Lisp-Makros können Sie Folgendes codieren:

(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))

Jetzt haben Sie eine &&, die Sie wie eine Funktion aufrufen können und die keine Formulare auswertet, die Sie an sie übergeben, es sei denn, sie sind alle wahr.

Um zu sehen, wie nützlich dies ist, kontrastieren Sie:

(&& (very-cheap-operation)
    (very-expensive-operation)
    (operation-with-serious-side-effects))

und:

and(very_cheap_operation(),
    very_expensive_operation(),
    operation_with_serious_side_effects());

Andere Dinge, die Sie mit Makros tun können, sind das Erstellen neuer Schlüsselwörter und / oder Minisprachen (siehe (loop ...)Makro für ein Beispiel) und das Integrieren anderer Sprachen in lisp. Sie können beispielsweise ein Makro schreiben, mit dem Sie Folgendes sagen können:

(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")

Und das geht nicht einmal in Reader-Makros .

Hoffe das hilft.

dsm
quelle
Ich denke es sollte sein: "(bewerben &&, @ exprs); dies und könnte falsch sein!"
Svante
1
@svante - in zweierlei Hinsicht: Erstens ist && ein Makro, keine Funktion; Anwenden funktioniert nur bei Funktionen. Zweitens: Übernehmen Nehmen Sie eine Liste der zu übergebenden Argumente, damit Sie eines von "(funcall fn, @ exprs)", "(apply fn (list, @ exprs)" oder "(apply fn, @ exprs nil)" nicht möchten "(fn anwenden, @ exprs)".
Aaron
(and ...wird Ausdrücke auswerten, bis man als falsch bewertet, beachten Sie, dass Nebenwirkungen, die durch die falsche Auswertung erzeugt werden, auftreten, nur die nachfolgenden Ausdrücke werden übersprungen.
ocodo
31

Ich glaube nicht, dass ich jemals gesehen habe, wie Lisp-Makros besser erklärt wurden als von diesem Kollegen: http://www.defmacro.org/ramblings/lisp.html

Rayne
quelle
3
Besonders wenn Sie einen Java / XML-Hintergrund haben.
Sonnen
1
Was für eine Freude, das an einem Samstagnachmittag auf meiner Couch liegend zu lesen! Sehr klar geschrieben und organisiert.
Colin
10

Überlegen Sie, was Sie in C oder C ++ mit Makros und Vorlagen tun können. Sie sind sehr nützliche Tools zum Verwalten von sich wiederholendem Code, aber sie sind in schwerwiegender Weise eingeschränkt.

  • Die eingeschränkte Makro- / Vorlagensyntax schränkt ihre Verwendung ein. Beispielsweise können Sie keine Vorlage schreiben, die zu einer anderen Klasse als einer Klasse oder Funktion erweitert wird. Makros und Vorlagen können interne Daten nicht einfach verwalten.
  • Die komplexe, sehr unregelmäßige Syntax von C und C ++ macht es schwierig, sehr allgemeine Makros zu schreiben.

Lisp- und Lisp-Makros lösen diese Probleme.

  • Lisp-Makros sind in Lisp geschrieben. Sie haben die volle Kraft von Lisp, um das Makro zu schreiben.
  • Lisp hat eine sehr regelmäßige Syntax.

Sprechen Sie mit allen, die C ++ beherrschen, und fragen Sie sie, wie lange sie damit verbracht haben, die gesamte Fudgery für Vorlagen zu erlernen, die sie für die Metaprogrammierung von Vorlagen benötigen. Oder all die verrückten Tricks in (ausgezeichneten) Büchern wie Modern C ++ Design , die immer noch schwer zu debuggen und (in der Praxis) zwischen realen Compilern nicht portierbar sind, obwohl die Sprache seit einem Jahrzehnt standardisiert ist. All das schmilzt dahin, wenn die Sprache, die Sie für die Metaprogrammierung verwenden, dieselbe Sprache ist, die Sie für die Programmierung verwenden!

Matt Curtis
quelle
11
Um fair zu sein, besteht das Problem bei der Metaprogrammierung von C ++ - Vorlagen nicht darin, dass die Metaprogrammierungssprache anders ist , sondern dass sie schrecklich ist - sie wurde nicht so sehr entworfen, als dass sie in einer viel einfacheren Vorlagenfunktion entdeckt wurde.
Brooks Moses
@Brooks Sicher. Neue Funktionen sind nicht immer schlecht. Leider ist es in einer langsamen, vom Ausschuss gesteuerten Sprache schwierig, sie zu beheben, wenn sie es sind. Es ist eine Schande, dass so viele der modernen nützlichen neuen Funktionen von C ++ in einer Sprache geschrieben sind, die nur wenige lesen können, und es gibt eine große Lücke zwischen dem durchschnittlichen Programmierer und dem "Hohepriester".
Matt Curtis
2
@downvoter: Wenn etwas mit meiner Antwort nicht stimmt, hinterlasse bitte einen Kommentar, damit wir alle das Wissen teilen können.
Matt Curtis
10

Ein lisp-Makro nimmt ein Programmfragment als Eingabe. Dieses Programmfragment stellt eine Datenstruktur dar, die beliebig bearbeitet und transformiert werden kann. Am Ende gibt das Makro ein weiteres Programmfragment aus, und dieses Fragment wird zur Laufzeit ausgeführt.

C # verfügt nicht über eine Makrofunktion. Eine Entsprechung wäre jedoch, wenn der Compiler den Code in einen CodeDOM-Baum analysieren und an eine Methode übergeben würde, die diesen in ein anderes CodeDOM umwandelt, das dann in IL kompiliert wird.

Dies könnte verwendet werden, um "Zucker" for each-Syntax wie die -statement- usingKlausel, linq -expressions usw. selectals Makros zu implementieren , die sich in den zugrunde liegenden Code umwandeln.

Wenn Java Makros hätte, könnten Sie die Linq-Syntax in Java implementieren, ohne dass Sun die Basissprache ändern muss.

Hier ist ein Pseudocode, wie ein Makro im Lisp-Stil in C # zur Implementierung usingaussehen könnte:

define macro "using":
    using ($type $varname = $expression) $block
into:
    $type $varname;
    try {
       $varname = $expression;
       $block;
    } finally {
       $varname.Dispose();
    }
JacquesB
quelle
Nun , da es tatsächlich ist ein Lisp-Stil Makroprozessor für C #, würde ich darauf hinweisen, dass ein Makro für usingwürde wie folgt aussehen ;)
Qwertie
9

Da die vorhandenen Antworten gute konkrete Beispiele geben, die erklären, was und wie Makros erreichen, wäre es vielleicht hilfreich, einige der Gedanken darüber zusammenzufassen, warum die Makrofunktion im Vergleich zu anderen Sprachen einen erheblichen Gewinn darstellt . zuerst aus diesen Antworten, dann aus einer anderen:

... in C müssten Sie einen benutzerdefinierten Vorprozessor schreiben [der sich wahrscheinlich als ausreichend kompliziertes C-Programm qualifizieren würde ] ...

- Vatine

Sprechen Sie mit allen, die C ++ beherrschen, und fragen Sie sie, wie lange sie damit verbracht haben, die gesamte Fudgery für Vorlagen zu erlernen, die sie für die Metaprogrammierung von Vorlagen benötigen [was immer noch nicht so leistungsfähig ist].

- Matt Curtis

... in Java müssen Sie sich mit dem Weben von Bytecodes hacken, obwohl einige Frameworks wie AspectJ es Ihnen ermöglichen, dies mit einem anderen Ansatz zu tun, ist es im Grunde ein Hack.

- Miguel Ping

DOLIST ähnelt Perls foreach oder Pythons for. Java hat eine ähnliche Art von Schleifenkonstrukt mit der "erweiterten" for-Schleife in Java 1.5 als Teil von JSR-201 hinzugefügt. Beachten Sie, welchen Unterschied Makros machen. Ein Lisp-Programmierer, der ein gemeinsames Muster in seinem Code bemerkt, kann ein Makro schreiben, um sich selbst eine Abstraktion dieses Musters auf Quellenebene zu geben. Ein Java-Programmierer, der das gleiche Muster bemerkt, muss Sun davon überzeugen, dass diese bestimmte Abstraktion es wert ist, der Sprache hinzugefügt zu werden. Dann muss Sun einen JSR veröffentlichen und eine branchenweite "Expertengruppe" einberufen, um alles zu klären. Dieser Prozess dauert laut Sun durchschnittlich 18 Monate. Danach müssen alle Compiler-Autoren ihre Compiler aktualisieren, um die neue Funktion zu unterstützen. Und selbst wenn der Lieblingscompiler des Java-Programmierers die neue Version von Java unterstützt, Wahrscheinlich können sie die neue Funktion erst dann verwenden, wenn sie die Quellkompatibilität mit älteren Java-Versionen unterbrechen dürfen. Ein Ärger, den Common Lisp-Programmierer innerhalb von fünf Minuten selbst lösen können, plagt Java-Programmierer seit Jahren.

- Peter Seibel in "Practical Common Lisp"

ZakW
quelle
8

Ich bin mir nicht sicher, ob ich jedem (ausgezeichneten) Beitrag einen Einblick geben kann, aber ...

Lisp-Makros funktionieren aufgrund der Lisp-Syntax hervorragend.

Lisp ist eine sehr normale Sprache (denken Sie an alles ist eine Liste ); Mit Makros können Sie Daten und Code gleich behandeln (zum Ändern von Lisp-Ausdrücken sind keine String-Analyse oder andere Hacks erforderlich). Sie kombinieren diese beiden Funktionen und haben eine sehr saubere Möglichkeit, Code zu ändern.

Bearbeiten: Was ich sagen wollte ist, dass Lisp homoikonisch ist , was bedeutet, dass die Datenstruktur für ein Lisp-Programm in Lisp selbst geschrieben ist.

Am Ende haben Sie also die Möglichkeit, einen eigenen Codegenerator über der Sprache zu erstellen, indem Sie die Sprache selbst mit all ihrer Kraft verwenden (z. B. müssen Sie sich in Java mit dem Bytecode-Weben hacken, obwohl einige Frameworks wie AspectJ dies zulassen Tun Sie dies mit einem anderen Ansatz, es ist im Grunde ein Hack).

In der Praxis bauen Sie mit Makros Ihre eigene Minisprache auf lisp auf, ohne zusätzliche Sprachen oder Werkzeuge lernen zu müssen und die volle Kraft der Sprache selbst zu nutzen.

Miguel Ping
quelle
1
Es ist ein aufschlussreicher Kommentar, aber diese Idee, dass "alles eine Liste ist", könnte Neulinge erschrecken. Um eine Liste zu verstehen, müssen Sie die Nachteile, Autos, CDRs und Zellen verstehen. Genauer gesagt besteht Lisp aus S-expressionsnicht Listen.
Ribamar
6

Lisp-Makros stellen ein Muster dar, das in fast jedem größeren Programmierprojekt vorkommt. Schließlich haben Sie in einem großen Programm einen bestimmten Codeabschnitt, in dem Sie erkennen, dass es für Sie einfacher und weniger fehleranfällig wäre, ein Programm zu schreiben, das Quellcode als Text ausgibt, den Sie dann einfach einfügen können.

In Python haben Objekte zwei Methoden __repr__und __str__. __str__ist einfach die vom Menschen lesbare Darstellung. __repr__Gibt eine Darstellung zurück, die gültiger Python-Code ist, dh etwas, das als gültiger Python in den Interpreter eingegeben werden kann. Auf diese Weise können Sie kleine Python-Schnipsel erstellen, die gültigen Code generieren, der in Ihre eigentliche Quelle eingefügt werden kann.

In Lisp wurde dieser gesamte Prozess vom Makrosystem formalisiert. Sicher, es ermöglicht Ihnen, Erweiterungen der Syntax zu erstellen und alle möglichen ausgefallenen Dinge zu tun, aber die tatsächliche Nützlichkeit wird durch die obigen Ausführungen zusammengefasst. Natürlich hilft es, dass Sie mit dem Lisp-Makrosystem diese "Schnipsel" mit der vollen Leistung der gesamten Sprache bearbeiten können.

dnolen
quelle
1
Ihr erster Absatz ist für einen Lisp-Außenstehenden sehr klar, was wichtig ist.
Wildcard
5

Kurz gesagt, Makros sind Transformationen von Code. Sie ermöglichen die Einführung vieler neuer Syntaxkonstrukte. Betrachten Sie beispielsweise LINQ in C #. In lisp gibt es ähnliche Spracherweiterungen, die von Makros implementiert werden (z. B. eingebautes Schleifenkonstrukt, Iteration). Makros verringern die Codeduplizierung erheblich. Makros ermöglichen das Einbetten von «kleinen Sprachen» (z. B. wenn in c # / java XML zum Konfigurieren verwendet wird, kann in lisp dasselbe mit Makros erreicht werden). Makros können Schwierigkeiten bei der Verwendung von Bibliotheken verbergen.

ZB kann man in lisp schreiben

(iter (for (id name) in-clsql-query "select id, name from users" on-database *users-database*)
      (format t "User with ID of ~A has name ~A.~%" id name))

Dadurch werden alle Datenbankdaten (Transaktionen, ordnungsgemäßes Schließen der Verbindung, Abrufen von Daten usw.) ausgeblendet, während in C # SqlConnections, SqlCommands erstellt, SqlParameters zu SqlCommands hinzugefügt, SqlDataReaders wiederholt und ordnungsgemäß geschlossen werden müssen.

dmitry_vk
quelle
3

Während das vor allem erklärt, was Makros sind und sogar coole Beispiele haben, denke ich, dass der Hauptunterschied zwischen einem Makro und einer normalen Funktion darin besteht, dass LISP alle Parameter zuerst auswertet, bevor die Funktion aufgerufen wird. Bei einem Makro ist es umgekehrt. LISP übergibt die nicht bewerteten Parameter an das Makro. Wenn Sie beispielsweise (+ 1 2) an eine Funktion übergeben, erhält die Funktion den Wert 3. Wenn Sie dies an ein Makro übergeben, erhält sie eine Liste (+ 1 2). Dies kann verwendet werden, um alle Arten von unglaublich nützlichen Dingen zu erledigen.

  • Hinzufügen einer neuen Kontrollstruktur, z. B. Schleife oder Dekonstruktion einer Liste
  • Messen Sie die Zeit, die zum Ausführen einer übergebenen Funktion benötigt wird. Bei einer Funktion wird der Parameter ausgewertet, bevor die Steuerung an die Funktion übergeben wird. Mit dem Makro können Sie Ihren Code zwischen Start und Stopp Ihrer Stoppuhr verbinden. Das Folgende hat genau den gleichen Code in einem Makro und einer Funktion und die Ausgabe ist sehr unterschiedlich. Hinweis: Dies ist ein erfundenes Beispiel, und die Implementierung wurde so ausgewählt, dass sie identisch ist, um den Unterschied besser hervorzuheben.

    (defmacro working-timer (b) 
      (let (
            (start (get-universal-time))
            (result (eval b))) ;; not splicing here to keep stuff simple
        ((- (get-universal-time) start))))
    
    (defun my-broken-timer (b)
      (let (
            (start (get-universal-time))
            (result (eval b)))    ;; doesn't even need eval
        ((- (get-universal-time) start))))
    
    (working-timer (sleep 10)) => 10
    
    (broken-timer (sleep 10)) => 0
Jörg Schmücker
quelle
Übrigens hat Scala der Sprache Makros hinzugefügt. Obwohl ihnen die Schönheit der Lisp-Makros fehlt, weil die Sprache nicht homoikonisch ist, lohnt es sich auf jeden Fall, sie zu untersuchen, und die abstrakten Syntaxbäume, die sie bereitstellen, könnten am Ende einfacher zu verwenden sein. Es ist zu früh für mich zu sagen, welches Makrosystem ich bevorzuge.
Jörg Schmücker
2
"LISP übergibt die nicht bewerteten Parameter an das Makro" schließlich eine Antwort, die es klar sagt. Aber Sie haben die zweite Hälfte davon vergessen: und das Ergebnis eines Makros ist ein transformierter Code, der vom System anstelle des Originals als Ganzes ausgewertet wird, als wäre er an erster Stelle vorhanden (es sei denn, es ist selbst wieder ein Aufruf an ein Makro, das dieses Mal auch von diesem Makro transformiert wird ).
Will Ness
0

Ich habe dies aus dem allgemeinen Lisp-Kochbuch erhalten, aber ich denke, es hat erklärt, warum Lisp-Makros auf nette Weise gut sind.

"Ein Makro ist ein gewöhnlicher Teil des Lisp-Codes, der mit einem anderen Teil des mutmaßlichen Lisp-Codes arbeitet und ihn in eine ausführbare Datei (eine Version, die der ausführbaren Version näher kommt) übersetzt. Das mag etwas kompliziert klingen. Geben wir also ein einfaches Beispiel. Angenommen, Sie möchten eine Version von setq, die zwei Variablen auf den gleichen Wert setzt. Wenn Sie also schreiben

(setq2 x y (+ z 3))

wenn z=8sowohl x als auch y auf 11 gesetzt sind. (Ich kann mir keine Verwendung dafür vorstellen, aber es ist nur ein Beispiel.)

Es sollte offensichtlich sein, dass wir setq2 nicht als Funktion definieren können. Wenn x=50und y=-5, würde diese Funktion die Werte 50, -5 und 11 erhalten; es hätte keine Kenntnis davon, welche Variablen gesetzt werden sollten. Was wir wirklich sagen wollen, ist: Wenn Sie (das Lisp-System) sehen (setq2 v1 v2 e), behandeln Sie es als gleichwertig mit (progn (setq v1 e) (setq v2 e)). Eigentlich ist das nicht ganz richtig, aber es wird vorerst reichen. Ein Makro ermöglicht es uns, genau dies zu tun, indem wir ein Programm zum Transformieren des Eingabemusters (setq2 v1 v2 e)"in das Ausgabemuster" angeben (progn ...).

Wenn Sie das nett fanden, können Sie hier weiterlesen : http://cl-cookbook.sourceforge.net/macros.html

Nilsi
quelle
1
Es ist möglich, setq2als Funktion zu definieren, ob xund yals Referenz übergeben werden. Ich weiß jedoch nicht, ob es in CL möglich ist. Also für jemanden, der Lisps oder CL nicht besonders kennt, ist dies kein sehr anschauliches Beispiel IMO
neoascetic
Die Übergabe von @neoascetic CL-Argumenten erfolgt nur nach Wert (aus diesem Grund werden in erster Linie Makros benötigt). Einige Werte sind jedoch Zeiger (wie Listen).
Will Ness
-5

In Python haben Sie Dekoratoren, Sie haben im Grunde eine Funktion, die eine andere Funktion als Eingabe übernimmt. Sie können tun, was immer Sie wollen: Rufen Sie die Funktion auf, tun Sie etwas anderes, verpacken Sie den Funktionsaufruf in eine Ressourcenerfassungsversion usw., aber Sie können nicht in diese Funktion hineinschauen. Angenommen, wir wollten es leistungsfähiger machen, sagen, Ihr Dekorateur hat den Code der Funktion als Liste erhalten, dann können Sie die Funktion nicht nur so ausführen, wie sie ist, sondern Sie können jetzt Teile davon ausführen, Zeilen der Funktion neu anordnen usw.

Steinmetall
quelle