357 Stimmen

Was macht Lisp-Makros so besonders?

Lesen Paul Grahams Aufsätze über Programmiersprachen würde man meinen, dass Lisp-Makros sind der einzig richtige Weg. Als vielbeschäftigter Entwickler, der auf anderen Plattformen arbeitet, hatte ich bisher nicht das Privileg, Lisp-Makros zu verwenden. Bitte erklären Sie mir, was diese Funktion so leistungsfähig macht, denn ich möchte sie verstehen.

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

373voto

gte525u Punkte 4386

Um es kurz zu machen: Makros werden zur Definition von Sprachsyntax-Erweiterungen für Common Lisp oder Domain Specific Languages (DSLs) verwendet. Diese Sprachen werden direkt in den bestehenden Lisp-Code eingebettet. Nun können die DSLs eine Lisp-ähnliche Syntax haben (wie Peter Norvig's Prolog-Interpreter für Common Lisp) oder völlig anders (z. B. Infix-Notation Mathematik für Clojure).

Hier ist ein konkreteres Beispiel:
Python verfügt über eine in die Sprache integrierte Listenverarbeitung. Dies gibt eine einfache Syntax für einen häufigen Fall. Die Zeile

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

ergibt eine Liste mit allen geraden Zahlen zwischen 0 und 9. Zurück in der Python 1.5 gab es keine solche Syntax; man verwendete eher so etwas wie diese:

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

Beide sind funktional gleichwertig. Nehmen wir an, Lisp habe ein sehr begrenztes Schleifenmakro, das nur Iterationen durchführt, und keine einfache Möglichkeit, das Äquivalent von Listenauflösungen zu realisieren.

In Lisp könnte man das Folgende schreiben. Ich sollte anmerken, dass dieses erfundene Beispiel so gewählt wurde, dass es mit dem Python-Code identisch ist und kein gutes Beispiel für Lisp-Code darstellt.

;; 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 fortfahre, sollte ich besser erklären, was ein Makro ist. Es ist eine Transformation, die am Code durchgeführt wird von Code. Das heißt, ein vom Interpreter (oder Compiler) gelesenes Codestück, das Code als Argument aufnimmt, bearbeitet und das Ergebnis zurückgibt, das dann an Ort und Stelle ausgeführt wird.

Das ist natürlich eine Menge Tipparbeit und Programmierer sind faul. Also könnten wir eine DSL für Listenauflösungen definieren. Tatsächlich verwenden wir bereits ein Makro (das Schleifenmakro).

Lisp definiert eine Reihe von speziellen Syntaxformen. Das Anführungszeichen ( ' ) zeigt an, dass das nächste Token ein Literal ist. Die Quasiquote oder der Backtick ( ` ) zeigt an, dass das nächste Token ein Literal mit Escapes ist. Escape-Zeichen werden durch den Komma-Operator angezeigt. Das Literal '(1 2 3) ist das Äquivalent zu Pythons [1, 2, 3] . Sie können sie einer anderen Variablen zuweisen oder sie an ihrer Stelle verwenden. Sie können sich vorstellen `(1 2 ,x) als das Äquivalent zu Pythons [1, 2, x] donde x ist eine zuvor definierte Variable. Diese Listennotation ist Teil der Magie, die in Makros steckt. Der zweite Teil ist der Lisp-Reader, der auf intelligente Weise Makros durch Code ersetzt, aber das wird am besten weiter unten erläutert:

Wir können also ein Makro definieren, das lcomp (kurz für Listenverständnis). Die Syntax entspricht genau dem Python, das 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 es in der Befehlszeile ausführen:

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

Ziemlich toll, oder? Aber das ist noch nicht alles. Sie haben einen Mechanismus, oder einen Pinsel, wenn Sie so wollen. Sie können jede Syntax verwenden, die Sie sich vorstellen können. Wie Python oder C#s with Syntax. Oder die LINQ-Syntax von .NET. Letztendlich ist es das, was die Leute an Lisp reizt - ultimative Flexibilität.

115voto

VonC Punkte 1117238

Sie finden eine umfassende Debatte über Lisp-Makro hier .

Eine interessante Teilmenge dieses Artikels:

In den meisten Programmiersprachen ist die Syntax komplex. Makros müssen die Programmsyntax auseinandernehmen, analysieren und wieder zusammensetzen. Da sie keinen Zugriff auf den Parser des Programms haben, müssen sie sich auf Heuristiken und Vermutungen stützen. Manchmal ist ihre Schnellanalyse falsch, und dann gehen sie kaputt.

Aber Lisp ist anders. Lisp-Makros faire Zugriff auf den Parser haben, und es ist ein wirklich einfacher Parser. Ein Lisp-Makro ist keine Zeichenkette, sondern ein vorbereiteter Quelltext in Form einer Liste, denn der Quelltext eines Lisp-Programms ist keine Zeichenkette, sondern eine Liste. Und Lisp-Programme sind wirklich gut darin, Listen auseinanderzunehmen und wieder zusammenzusetzen. Sie tun dies zuverlässig, jeden Tag.

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

  (setf x whatever)

die den Wert des Symbols "x" auf den Wert des Ausdrucks "whatever" setzt.

Lisp verfügt auch über Listen; Sie können die Funktionen "car" und "cdr" verwenden, um das erste Element einer Liste bzw. den Rest der Liste zu erhalten.

Was aber, wenn Sie das erste Element einer Liste durch einen neuen Wert ersetzen wollen? Dafür gibt es eine Standardfunktion, deren Name noch schlimmer ist als "Auto". Sie heißt "rplaca". Aber Sie müssen sich "rplaca" nicht merken, denn Sie können schreiben

  (setf (car somelist) whatever)

um das Auto von somelist einzustellen.

Was hier wirklich passiert ist, dass "setf" ein Makro ist. Bei der Kompilierung prüft es seine Argumente und sieht, dass das erste die Form (car SOMETHING) hat. Es sagt zu sich selbst: "Oh, der Programmierer versucht, das Auto von irgendetwas zu setzen. Die dafür zu verwendende Funktion ist 'rplaca'." Und es schreibt den Code an dieser Stelle stillschweigend um in:

  (rplaca somelist whatever)

60voto

Vatine Punkte 19955

Common Lisp-Makros erweitern im Wesentlichen die "syntaktischen Primitive" Ihres Codes.

In C beispielsweise funktioniert das switch/case-Konstrukt nur mit ganzzahligen Typen, und wenn Sie es für Fließkommazahlen oder Zeichenketten verwenden wollen, müssen Sie mit verschachtelten if-Anweisungen und expliziten Vergleichen arbeiten. Es gibt auch keine Möglichkeit, ein C-Makro zu schreiben, das diese Aufgabe für Sie erledigt.

Da aber ein Lisp-Makro (im Wesentlichen) ein Lisp-Programm ist, das Codeschnipsel als Eingabe annimmt und Code zurückgibt, der den "Aufruf" des Makros ersetzt, können Sie Ihr "Primitiv"-Repertoire so weit ausdehnen, wie Sie wollen, was in der Regel zu einem besser lesbaren Programm führt.

Um dasselbe in C zu tun, müssten Sie einen eigenen Präprozessor schreiben, der Ihren ursprünglichen (nicht ganz C-konformen) Quelltext aufnimmt und etwas ausgibt, das ein C-Compiler verstehen kann. Das ist kein falscher Weg, aber es ist nicht unbedingt der einfachste.

48voto

dsm Punkte 10073

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

expr1 && expr2 && expr3 ...

Das bedeutet Folgendes: Bewerten Sie expr1 und, sollte es wahr sein, bewerten expr2 , etc.

Versuchen Sie nun, dies zu machen && in eine Funktion zu integrieren... das ist richtig, das können Sie nicht. Der Aufruf von etwas wie:

and(expr1, expr2, expr3)

Wird alle drei bewerten exprs bevor er eine Antwort gibt, unabhängig davon, ob expr1 war falsch!

Mit Lisp-Makros können Sie so etwas programmieren wie:

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

Sie haben jetzt eine && die Sie wie eine Funktion aufrufen können und die keine Formen auswertet, die Sie ihr ü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());

Mit Makros können Sie auch neue Schlüsselwörter und/oder Mini-Sprachen erstellen (siehe auch die (loop ...) Makro für ein Beispiel), das andere Sprachen in Lisp integriert, könnten Sie zum Beispiel ein Makro schreiben, mit dem Sie etwas sagen können wie:

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

Und damit sind wir noch nicht einmal beim Leser-Makros .

Ich hoffe, das hilft.

34voto

Rayne Punkte 29813

Ich glaube, ich habe noch nie gesehen, dass Lisp-Makros besser erklärt werden als von diesem Mann: http://www.defmacro.org/ramblings/lisp.html

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X