Wie nützlich sind Lisp-Makros?

22

Mit Common Lisp können Sie Makros schreiben, die die gewünschte Quelltransformation ausführen.

Das Schema gibt Ihnen ein hygienisches Musteranpassungssystem, mit dem Sie auch Transformationen durchführen können. Wie nützlich sind Makros in der Praxis? Paul Graham sagte in Beating the Averages :

Der Quellcode des Viaweb-Editors bestand wahrscheinlich zu 20-25% aus Makros.

Was machen die Leute eigentlich mit Makros?

compman
quelle
Ich denke das fällt definitiv gut subjektiv aus , ich habe deine Frage zum Formatieren bearbeitet. Dies könnte ein Duplikat sein, aber ich konnte keines finden.
Tim Post
1
Alles, was sich wiederholt und anscheinend nicht in eine Funktion passt, würde ich vermuten.
2
Sie können Makros verwenden, um Lisp in eine andere Sprache mit beliebiger Syntax und Semantik umzuwandeln
SK-logic
Hier lohnt sich ein Blick auf programmers.stackexchange.com/questions/81202/… , aber es ist kein Duplikat.
David Thornley

Antworten:

15

Werfen Sie einen Blick auf diesen Beitrag von Matthias Felleisen zur LL1-Diskussionsliste im Jahr 2002. Er schlägt drei Hauptverwendungen für Makros vor:

  1. Daten-Subsprachen : Ich kann einfach aussehende Ausdrücke schreiben und komplexe verschachtelte Listen / Arrays / Tabellen mit Anführungszeichen, Anführungszeichen usw. erstellen, die ordentlich mit Makros verkleidet sind.
  2. Bindungskonstrukte : Ich kann neue Bindungskonstrukte mit Makros einführen. Das hilft mir, Lambdas loszuwerden und Dinge, die zusammen gehören, näher zusammen zu stellen.
  3. Neuordnung der Auswertung : Ich kann Konstrukte einführen, die die Auswertung von Ausdrücken nach Bedarf verzögern / verschieben. Denken Sie an Schleifen, neue Bedingungen, Verzögerung / Kraft usw. [Hinweis: In Haskell oder einer faulen Sprache ist diese unnötig.]
John Clements
quelle
18

Ich benutze meistens Makros, um zeitsparende neue Sprachkonstrukte hinzuzufügen, für die sonst ein Haufen Code erforderlich wäre.

Zum Beispiel wollte ich kürzlich einen Imperativ for-loop, der C ++ / Java ähnelt. Da Clojure jedoch eine funktionale Sprache ist, wurde sie nicht sofort geliefert. Also habe ich es einfach als Makro implementiert:

(defmacro for-loop [[sym init check change :as params] & steps]
  `(loop [~sym ~init value# nil]
     (if ~check
       (let [new-value# (do ~@steps)]
         (recur ~change new-value#))
       value#)))

Und jetzt kann ich tun:

 (for-loop [i 0 , (< i 10) , (inc i)] 
   (println i))

Und da haben Sie es - ein neues Allzweck-Sprachkonstrukt zur Kompilierungszeit in sechs Codezeilen.

mikera
quelle
13

Was machen die Leute eigentlich mit Makros?

Spracherweiterungen oder DSLs schreiben.

Um ein Gefühl dafür in Lisp-ähnlichen Sprachen zu bekommen, studieren Sie Racket , das verschiedene Sprachvarianten bietet: Typed Racket, R6RS und Datalog.

Siehe auch die Boo-Sprache, mit der Sie auf die Compiler-Pipeline zugreifen können, um domänenspezifische Sprachen über Makros zu erstellen.

Robert Harvey
quelle
4

Hier sind einige Beispiele:

Planen:

  • definefür Funktionsdefinitionen. Grundsätzlich ist es ein kürzerer Weg, eine Funktion zu definieren.
  • let zum Erstellen von Variablen mit lexikalischem Gültigkeitsbereich.

Clojure:

  • defn, nach seinen Unterlagen:

    Entspricht (def name (fn [params *] exprs *)) oder (def name (fn ([params *] exprs *) +)) mit einem beliebigen Dokument-String oder Attribut, das den var-Metadaten hinzugefügt wurde

  • for: Listenverständnisse
  • defmacro: ironisch?
  • defmethod, defmulti: Arbeiten mit mehreren Methoden
  • ns

Viele dieser Makros erleichtern das Schreiben von Code auf einer abstrakteren Ebene. Ich denke, Makros ähneln in vielerlei Hinsicht der Syntax in Nicht-Lisps.

Die Zeichnungsbibliothek Incanter bietet Makros für einige komplexe Datenmanipulationen.


quelle
4

Makros sind nützlich, um einige Muster einzubetten.

Zum Beispiel definiert Common Lisp die whileSchleife nicht, sondern hatdo eine, mit der sie definiert werden kann.

Hier ist ein Beispiel von On Lisp .

(defmacro while (test &body body)
  `(do ()
       ((not ,test))
     ,@body))

(let ((a 0))
  (while (< a 10)
    (princ (incf a))))

Dies gibt "12345678910" aus und wenn Sie versuchen zu sehen, was passiert mit macroexpand-1:

(macroexpand-1 '(while (< a 10) (princ (incf a))))

Dies wird zurückkehren:

(DO () ((NOT (< A 10))) (PRINC (INCF A)))

Dies ist ein einfaches Makro, aber wie bereits erwähnt, werden sie normalerweise zum Definieren neuer Sprachen oder DSLs verwendet. Anhand dieses einfachen Beispiels können Sie jedoch bereits versuchen, sich vorzustellen, wie Sie damit umgehen können.

Das loop Makro ist ein gutes Beispiel dafür, was Makros können.

(loop for i from 0 to 10
      if (and (= (mod i 2) 0) i)
        collect it)
=> (0 2 4 6 8 10)
(loop for i downfrom 10 to 0
      with a = 2
      collect (* a i))
=> (20 18 16 14 12 10 8 6 4 2 0)               

Common Lisp verfügt über eine andere Art von Makros, das sogenannte Reader-Makro, mit dem Sie die Interpretation des Codes durch den Reader ändern können. Sie können also # {und #} verwenden, um Trennzeichen wie # (und #) zu verwenden.

Daimrod
quelle
3

Folgendes verwende ich zum Debuggen (in Clojure):

user=> (defmacro print-var [varname] `(println ~(name varname) "=" ~varname))
#'user/print-var
=> (def x (reduce * [1 2 3 4 5]))
#'user/x
=> (print-var x)
x = 120
nil

Ich musste mich in C ++ mit einer von Hand gerollten Hash-Tabelle auseinandersetzen, bei der die getMethode eine nicht konstante Zeichenfolgenreferenz als Argument verwendete, was bedeutet, dass ich sie nicht mit einem Literal aufrufen kann. Um das einfacher zu machen, habe ich Folgendes geschrieben:

#define LET(name, value, body)  \
    do {                        \
        string name(value);     \
        body;                   \
        assert(name == value);  \
    } while (false)

Es ist zwar unwahrscheinlich, dass dieses Problem auftaucht, aber ich finde es besonders schön, dass Sie Makros haben können, die ihre Argumente nicht zweimal auswerten, indem Sie beispielsweise einen Real einführen Let-Bindung . (Zugegeben, hier hätte ich mich zurechtfinden können).

Ich greife auch auf den furchtbar hässlichen Trick zurück, Sachen in ein Päckchen zu wickeln do ... while (false) so etwas dass man sie im damaligen Teil eines If verwenden kann und das übrige Teil immer noch wie erwartet funktioniert. Sie brauchen dies nicht in lisp, das ist eine Funktion von Makros, die auf Syntaxbäumen ausgeführt werden, anstatt von Strings (oder Token-Sequenzen, glaube ich, im Fall von C und C ++), die dann analysiert werden.

Es gibt einige eingebaute Threading-Makros, mit denen Sie Ihren Code so reorganisieren können, dass er sauberer gelesen wird ("Threading" wie "Code zusammenfügen", nicht Parallelität). Beispielsweise:

(->> (range 6) (filter even?) (map inc) (reduce *))

Es nimmt die erste Form an (range 6)und macht es zum letzten Argument der nächsten Form, (filter even?)die wiederum zum letzten Argument der nächsten Form usw. gemacht wird, so dass das Obige umgeschrieben wird

(reduce * (map inc (filter even? (range 6))))

Ich denke, das erste liest sich viel klarer: "Nehmen Sie diese Daten, machen Sie das, dann machen Sie das, dann machen Sie das andere und wir sind fertig", aber das ist subjektiv; Objektiv gesehen stimmt es, dass Sie die Operationen in der Reihenfolge lesen, in der sie ausgeführt werden (Faulheit wird ignoriert).

Es gibt auch eine Variante, bei der die vorherige Form als erstes (und nicht als letztes) Argument eingefügt wird. Ein Anwendungsfall ist die Arithmetik:

(-> 17 (- 2) (/ 3))

Liest als "nehmen Sie 17, subtrahieren Sie 2 und teilen Sie durch 3".

Apropos Arithmetik: Sie können ein Makro schreiben, das das Parsen von Infixnotationen ausführt, so dass Sie beispielsweise sagen können, dass (infix (17 - 2) / 3)es ausspuckt, (/ (- 17 2) 3)was den Nachteil hat, dass es weniger lesbar ist und den Vorteil hat, ein gültiger Lisp-Ausdruck zu sein. Das ist der Teil der DSL / Daten-Subsprache.

Jonas Kölker
quelle
1
Die Funktionsanwendung ist für mich viel sinnvoller als das Einfädeln, aber es ist sicherlich eine Gewohnheitssache. Gute Antwort.
Coredump