Suche…


Bemerkungen

Der Zweck von Makros

Makros dienen der Generierung von Code, der Umwandlung von Code und der Bereitstellung neuer Notationen. Diese neuen Notationen können besser geeignet sein, um das Programm besser auszudrücken, indem zum Beispiel Konstrukte auf Domänenebene oder ganze neue eingebettete Sprachen bereitgestellt werden.

Makros können den Quellcode selbsterklärender machen, das Debugging kann jedoch schwieriger sein. Als Daumenregel sollten Sie keine Makros verwenden, wenn dies von einer regulären Funktion ausgeführt wird. Wenn Sie sie verwenden, vermeiden Sie die üblichen Fallstricke und halten Sie sich an die häufig verwendeten Muster und Namenskonventionen.

Macroexpansion Order

Im Vergleich zu Funktionen werden Makros in umgekehrter Reihenfolge erweitert. äußerster zuerst, innerster letzter. Das bedeutet, dass standardmäßig kein inneres Makro zum Generieren der für ein äußeres Makro erforderlichen Syntax verwendet werden kann.

Bewertungsreihenfolge

Manchmal müssen Makros vom Benutzer bereitgestellte Formulare verschieben. Es muss sichergestellt werden, dass die Reihenfolge, in der sie ausgewertet werden, nicht geändert wird. Der Benutzer kann sich auf Nebeneffekte verlassen, die in der Reihenfolge auftreten.

Nur einmal auswerten

Bei der Erweiterung eines Makros muss der Wert desselben vom Benutzer angegebenen Formulars häufig mehr als einmal verwendet werden. Es ist möglich, dass das Formular Nebenwirkungen hat oder eine teure Funktion aufruft. Daher muss das Makro sicherstellen, dass solche Formulare nur einmal ausgewertet werden. Normalerweise geschieht dies, indem der Wert einer lokalen Variablen GENSYM wird (deren Name GENSYM ed GENSYM ).

Von Makros verwendete Funktionen mit EVAL-WHEN

Bei komplexen Makros werden oft Teile ihrer Logik in separaten Funktionen implementiert. Man muss jedoch bedenken, dass Makros erweitert werden, bevor der eigentliche Code kompiliert wird. Beim Kompilieren einer Datei stehen Funktionen und Variablen, die in derselben Datei definiert sind, während der Makroausführung standardmäßig nicht zur Verfügung. Alle Funktions- und Variablendefinitionen in derselben Datei, die von einem Makro verwendet werden, müssen in ein EVAL-WHEN . Das EVAL-WHEN sollte alle drei Zeiten festgelegt haben, wenn der eingeschlossene Code auch während des Ladens und der Laufzeit ausgewertet werden soll.

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defun foobar () ...))

Dies gilt nicht für Funktionen, die von der Erweiterung des Makros aufgerufen werden, nur diejenigen, die vom Makro selbst aufgerufen werden.

Häufige Makromuster

TODO: Vielleicht verschieben Sie die Erläuterungen in Anmerkungen und fügen Sie Beispiele separat hinzu

FOOF

In Common Lisp gibt es ein Konzept allgemeiner Verweise . Sie ermöglichen es einem Programmierer, Werte auf verschiedene "Stellen" zu setzen, als wären sie Variablen. Makros, die diese Fähigkeit nutzen, haben oft einen F Postfix im Namen. Der Ort ist normalerweise das erste Argument für das Makro.

Beispiele aus dem Standard: INCF , DECF , ROTATEF , SHIFTF , REMF .

Ein dummes Beispiel, ein Makro, das das Vorzeichen eines Nummernspeichers an einem Ort umdreht:

(defmacro flipf (place)
  `(setf ,place (- ,place)))

MIT-FOO

Makros, die eine Ressource erwerben und sicher freigeben, werden normalerweise mit einem WITH- Präfix bezeichnet. Das Makro sollte normalerweise die folgende Syntax verwenden:

(with-foo (variable details-of-the-foo...)
  body...)

Beispiele aus dem Standard: WITH-OPEN-FILE , WITH-OPEN-STREAM , WITH-INPUT-FROM-STRING , WITH-OUTPUT-TO-STRING .

Ein Ansatz zum Implementieren dieser Art von Makro, der einige der Nachteile der Namensverschmutzung und unbeabsichtigten Mehrfachauswertung vermeiden kann, ist die Implementierung einer funktionalen Version. Der erste Schritt beim Implementieren eines with-widget kann beispielsweise eine Funktion sein:

(defun call-with-widget (args function)
  (let ((widget (apply #'make-widget args))) ; obtain WIDGET
    (unwind-protect (funcall function widget)  ; call FUNCTION with WIDGET
      (cleanup widget)                         ; cleanup

Da es sich um eine Funktion handelt, gibt es keine Bedenken hinsichtlich des Umfangs der Namen innerhalb der Funktion oder des Lieferanten. Dadurch können Sie leicht ein entsprechendes Makro schreiben:

(defmacro with-widget ((var &rest args) &body body)
  `(call-with-widget (list ,@args) (lambda (,var) ,@body)))

DO-FOO

Makros, die etwas durchlaufen, werden oft mit einem DO Präfix bezeichnet. Die Makrosyntax sollte normalerweise in Form sein

(do-foo (variable the-foo-being-done return-value)
  body...)

Beispiele aus dem Standard: DOTIMES , DOLIST , DO-SYMBOLS .

FOOCASE, EFOOCASE, CFOOCASE

Makros, die eine Eingabe mit bestimmten Fällen abgleichen, werden häufig mit einem CASE -postfix benannt. Oft gibt es eine E...CASE -Variante, die einen Fehler signalisiert, wenn der Eingang mit keinem der Fälle übereinstimmt, und C...CASE , die einen dauerhaften Fehler signalisiert. Sie sollten eine ähnliche Syntax haben

(foocase input
  (case-to-match-against (optionally-some-params-for-the-case)
   case-body-forms...)
  more-cases...
  [(otherwise otherwise-body)])

Beispiele aus dem Standard: CASE , TYPECASE , HANDLER-CASE .

Beispielsweise ein Makro, das eine Zeichenfolge mit regulären Ausdrücken abgleichen und die Registergruppen an Variablen bindet. Verwendet CL-PPCRE für reguläre Ausdrücke.

(defmacro regexcase (input &body cases)
  (let ((block-sym (gensym "block"))
        (input-sym (gensym "input")))
    `(let ((,input-sym ,input))
       (block ,block-sym
         ,@(loop for (regex vars . body) in cases
                 if (eql regex 'otherwise)
                   collect `(return-from ,block-sym (progn ,vars ,@body))
                 else
                   collect `(cl-ppcre:register-groups-bind ,vars
                                (,regex ,input-sym)
                              (return-from ,block-sym
                                (progn ,@body))))))))

(defun test (input)
  (regexcase input
    ("(\\d+)-(\\d+)" (foo bar)
      (format t "Foo: ~a, Bar: ~a~%" foo bar))
    ("Foo: (\\w+)$" (foo)
      (format t "Foo: ~a.~%" foo))
    (otherwise (format t "Didn't match.~%"))))

(test "asd 23-234 qwe")
; Foo: 23, Bar: 234
(test "Foo: Foobar")
; Foo: Foobar.
(test "Foo: 43 - 23")
; Didn't match.

DEFINE-FOO, DEFFOO

Makros, die Dinge definieren, werden normalerweise entweder mit DEFINE- oder DEF Präfix bezeichnet.

Beispiele aus dem Standard: DEFUN , DEFMACRO , DEFINE-CONDITION .

Anaphorische Makros

Ein anaphorisches Makro ist ein Makro, das eine Variable (oft IT ) einführt, die das Ergebnis eines vom Benutzer angegebenen Formulars erfasst. Ein allgemeines Beispiel ist das anaphorische If, ​​das einer regulären IF ähnelt, aber auch die Variable IT definiert, die auf das Ergebnis der Testform verweist.

(defmacro aif (test-form then-form &optional else-form)
  `(let ((it ,test-form))
     (if it ,then-form ,else-form)))

(defun test (property plist)
  (aif (getf plist property)
       (format t "The value of ~s is ~a.~%" property it)
       (format t "~s wasn't in ~s!~%" property plist)))

(test :a '(:a 10 :b 20 :c 30))
; The value of :A is 10.
(test :d '(:a 10 :b 20 :c 30))
; :D wasn't in (:A 10 :B 20 :C 30)!

MACROEXPAND

Makroerweiterung ist der Prozess, aus dem Makros in tatsächlichen Code umgewandelt werden. Dies geschieht normalerweise als Teil des Kompilierungsprozesses. Der Compiler erweitert alle Makroformulare, bevor der Code tatsächlich kompiliert wird. Die Makroerweiterung geschieht auch während der Interpretation von Lisp-Code.

Man kann MACROEXPAND manuell aufrufen, MACROEXPAND zu sehen, worauf sich ein MACROEXPAND erweitert.

CL-USER> (macroexpand '(with-open-file (file "foo")
                        (do-something-with file)))
(LET ((FILE (OPEN "foo")) (#:G725 T))
  (UNWIND-PROTECT
      (MULTIPLE-VALUE-PROG1 (PROGN (DO-SOMETHING-WITH FILE)) (SETQ #:G725 NIL))
    (WHEN FILE (CLOSE FILE :ABORT #:G725))))

MACROEXPAND-1 ist dasselbe, wird jedoch nur einmal erweitert. Dies ist nützlich, wenn Sie versuchen, ein Makro-Formular zu verstehen, das zu einem anderen Makro-Formular erweitert wird.

CL-USER> (macroexpand-1 '(with-open-file (file "foo")
                          (do-something-with file)))
(WITH-OPEN-STREAM (FILE (OPEN "foo")) (DO-SOMETHING-WITH FILE))

Beachten Sie, dass weder MACROEXPAND noch MACROEXPAND-1 den Lisp-Code auf allen Ebenen erweitern. Sie erweitern nur das Makroformular der obersten Ebene. Um ein Formular auf allen Ebenen vollständig zu erweitern, ist ein Code-Walker erforderlich. Diese Möglichkeit ist im Common Lisp-Standard nicht vorgesehen.

Backquote - Schreiben von Code-Vorlagen für Makros

Makros Rückkehrcode. Da Code in Lisp aus Listen besteht, können Sie die regulären Listenbearbeitungsfunktionen verwenden, um ihn zu generieren.

;; A pointless macro
(defmacro echo (form)
  (list 'progn
        (list 'format t "Form: ~a~%" (list 'quote form))
        form))

Dies ist oft sehr schwer zu lesen, besonders bei längeren Makros. Mit dem Backquote-Lesermakro können zitierte Vorlagen geschrieben werden, die durch selektive Auswertung von Elementen ausgefüllt werden.

(defmacro echo (form)
  `(progn
     (format t "Form: ~a~%" ',form)
     ,form))

(macroexpand '(echo (+ 3 4)))
;=> (PROGN (FORMAT T "Form: ~a~%" '(+ 3 4)) (+ 3 4))

Diese Version sieht fast wie normaler Code aus. Die Kommas werden verwendet, um FORM auszuwerten. alles andere wird zurückgegeben wie es ist. Beachten Sie, dass das einfache Anführungszeichen in ',form außerhalb des Kommas steht und daher zurückgegeben wird.

Sie können auch ,@ verwenden ,@ um eine Liste in der Position zu verbinden.

(defmacro echo (&rest forms)
  `(progn
     ,@(loop for form in forms collect `(format t "Form: ~a~%" ,form))
     ,@forms))

(macroexpand '(echo (+ 3 4) 
                    (print "foo")
                    (random 10)))
;=> (PROGN
;    (FORMAT T "Form: ~a~%" (+ 3 4))
;    (FORMAT T "Form: ~a~%" (PRINT "foo"))
;    (FORMAT T "Form: ~a~%" (RANDOM 10))
;    (+ 3 4)
;    (PRINT "foo")
;    (RANDOM 10))

Backquote kann auch außerhalb von Makros verwendet werden.

Einzigartige Symbole, um Namenskonflikte in Makros zu verhindern

Für die Erweiterung eines Makros müssen häufig Symbole verwendet werden, die vom Benutzer nicht als Argumente übergeben wurden (z. B. als Namen für lokale Variablen). Es muss sichergestellt werden, dass solche Symbole nicht mit einem Symbol in Konflikt stehen, das der Benutzer im umgebenden Code verwendet.

Dies wird normalerweise durch die Verwendung von GENSYM , einer Funktion, die ein neues, nicht vernetztes Symbol zurückgibt.

Schlecht

Betrachten Sie das Makro unten. Es erstellt eine DOTIMES Schleife, die auch das Ergebnis des Körpers in einer Liste sammelt, die am Ende zurückgegeben wird.

(defmacro dotimes+collect ((var count) &body body)
  `(let ((result (list)))
     (dotimes (,var ,count (nreverse result))
       (push (progn ,@body) result))))

(dotimes+collect (i 5)
  (format t "~a~%" i)
  (* i i))
; 0
; 1
; 2
; 3
; 4
;=> (0 1 4 9 16)

Dies scheint in diesem Fall zu funktionieren, aber wenn der Benutzer einen Variablennamen RESULT , den er im Body verwendet, wären die Ergebnisse wahrscheinlich nicht das, was er erwartet. Betrachten Sie diesen Versuch, eine Funktion zu schreiben, die eine Liste der Summen aller ganzen Zahlen bis N sammelt:

(defun sums-upto (n)
  (let ((result 0))
    (dotimes+collect (i n)
      (incf result i))))

(sums-upto 10) ;=> Error!

Gut

Um das Problem zu beheben, müssen Sie GENSYM , um einen eindeutigen Namen für die RESULT Variable in der GENSYM zu generieren.

(defmacro dotimes+collect ((var count) &body body)
  (let ((result-symbol (gensym "RESULT")))
    `(let ((,result-symbol (list)))
       (dotimes (,var ,count (nreverse ,result-symbol))
         (push (progn ,@body) ,result-symbol)))))

(sums-upto 10) ;=> (0 1 3 6 10 15 21 28 36 45)

TODO: Wie man aus Strings Symbole macht

TODO: Probleme mit Symbolen in verschiedenen Paketen vermeiden

wenn-lassen, wann-lassen, -let Makros

Diese Makros vereinen Kontrollfluss und Bindung. Sie stellen eine Verbesserung gegenüber anaphorischen anaphorischen Makros dar, da sie dem Entwickler die Bedeutung durch Benennung vermitteln. Als solche wird ihre Verwendung gegenüber ihren anaphorischen Gegenstücken empfohlen.

(if-let (user (get-user user-id))
  (show-dashboard user)
  (redirect 'login-page))

FOO-LET Makros binden eine oder mehrere Variablen und verwenden diese Variablen dann als Testformular für die entsprechende Bedingung ( IF , WHEN ). Mehrere Variablen werden mit AND kombiniert. Der gewählte Zweig wird mit den aktiven Bindungen ausgeführt. Eine einfache Implementierung von IF-LET einer Variablen könnte etwa so aussehen:

(defmacro if-let ((var test-form) then-form &optional else-form)
  `(let ((,var ,test-form))
     (if ,var ,then-form ,else-form)))

(macroexpand '(if-let (a (getf '(:a 10 :b 20 :c 30) :a))
               (format t "A: ~a~%" a)
               (format t "Not found.~%")))
; (LET ((A (GETF '(:A 10 :B 20 :C 30) :A)))
;   (IF A
;       (FORMAT T "A: ~a~%" A)
;       (FORMAT T "Not found.~%")))

Eine Version, die mehrere Variablen unterstützt, ist in der Alexandria- Bibliothek verfügbar.

Verwenden von Makros zum Definieren von Datenstrukturen

Makros werden häufig verwendet, um Vorlagen für Datenstrukturen zu erstellen, die gemeinsamen Regeln folgen, aber unterschiedliche Felder enthalten können. Durch das Schreiben eines Makros können Sie die Angabe der detaillierten Konfiguration der Datenstruktur zulassen, ohne den Boilerplate-Code wiederholen zu müssen oder eine weniger effiziente Struktur (z. B. einen Hash) im Speicher zu verwenden, um die Programmierung zu vereinfachen.

Angenommen, wir möchten eine Anzahl von Klassen definieren, die eine Reihe unterschiedlicher Eigenschaften haben, jeweils mit einem Getter und Setter. Für einige (aber nicht alle) dieser Eigenschaften möchten wir, dass der Setter eine Methode für das Objekt aufruft, die ihn darüber informiert, dass die Eigenschaft geändert wurde. Obwohl Common LISP bereits eine Abkürzung für das Schreiben von Gettern und Setters hat, muss für das Schreiben eines standardmäßigen benutzerdefinierten Setters auf diese Weise normalerweise der Code kopiert werden, der die Benachrichtigungsmethode in jedem Setter aufruft . Durch die Definition eines Makros wird es jedoch viel einfacher:

(defmacro notifier (class slot) 
  "Defines a setf method in (class) for (slot) which calls the object's changed method."
   `(defmethod (setf ,slot) (val (item ,class))
     (setf (slot-value item ',slot) val)
     (changed item ',slot)))

(defmacro notifiers (class slots)
  "Defines setf methods in (class) for all of (slots) which call the object's changed method."
  `(progn 
     ,@(loop for s in slots collecting `(notifier ,class ,s))))

(defmacro defclass-notifier-slots (class nslots slots)  
  "Defines a class with (nslots) giving a list of slots created with notifiers, and (slots) giving a list of slots created with regular accessors."
  `(progn
     (defclass ,class () 
       ( ,@(loop for s in nslots collecting `(,s :reader ,s)) 
         ,@(loop for s in slots collecting `(,s :accessor ,s))))   
     (notifiers ,class ,nslots)))

Wir können jetzt schreiben (defclass-notifier-slots foo (bar baz qux) (waldo)) und sofort eine Klasse foo mit einem regulären Slot waldo (erstellt durch den zweiten Teil des Makros mit der Spezifikation (waldo :accessor waldo) ) und Schlitze bar , baz und qux mit Setzern, die den Anruf changed Verfahren (wobei der Getter durch den ersten Teil des Makros definiert ist, (bar :reader bar) , und die Einstelleinrichtung durch das aufgerufene notifier Makro).

Neben der Möglichkeit, mehrere Klassen, die sich so verhalten, mit einer großen Anzahl von Eigenschaften ohne Wiederholung schnell zu definieren, haben wir den üblichen Vorteil der Wiederverwendung von Code: Wenn wir uns später entscheiden, die Arbeitsweise der Notifier-Methoden zu ändern, können wir die Klasse einfach ändern Makro, und die Struktur jeder Klasse, die es verwendet, ändert sich.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow