common-lisp
macros
Zoeken…
Opmerkingen
Het doel van macro's
Macro's zijn bedoeld voor het genereren van code, het transformeren van code en het leveren van nieuwe notaties. Deze nieuwe notaties kunnen meer geschikt zijn om het programma beter tot uitdrukking te brengen, bijvoorbeeld door constructies op domeinniveau of geheel nieuwe ingebedde talen aan te bieden.
Macro's kunnen de broncode duidelijker maken, maar foutopsporing kan moeilijker worden gemaakt. Als vuistregel moet men geen macro's gebruiken als een normale functie voldoende is. Wanneer u ze wel gebruikt, vermijd dan de gebruikelijke valkuilen, probeer u te houden aan de veelgebruikte patronen en naamgevingsconventies.
Volgorde van macro-uitbreiding
In vergelijking met functies worden macro's in omgekeerde volgorde uitgevouwen; buitenste eerst, binnenste laatste. Dit betekent dat men standaard geen interne macro kan gebruiken om de syntaxis te genereren die vereist is voor een externe macro.
Evaluatie bestelling
Soms moeten macro's door gebruikers geleverde formulieren verplaatsen. Men moet ervoor zorgen dat de volgorde waarin ze worden geëvalueerd niet wordt gewijzigd. De gebruiker kan vertrouwen op bijwerkingen die in volgorde optreden.
Evalueer eenmalig
De uitbreiding van een macro moet vaak de waarde van hetzelfde door de gebruiker geleverde formulier meerdere keren gebruiken. Het is mogelijk dat het formulier bijwerkingen heeft, of het roept een dure functie op. Daarom moet de macro ervoor zorgen dat dergelijke formulieren slechts eenmaal worden geëvalueerd. Meestal wordt dit gedaan door de waarde toe te wijzen aan een lokale variabele (wiens naam GENSYM
ed is).
Functies die door macro's worden gebruikt met EVAL-WHEN
Complexe macro's hebben vaak delen van hun logica in afzonderlijke functies geïmplementeerd. Men moet echter niet vergeten dat macro's worden uitgebreid voordat de eigenlijke code wordt gecompileerd. Bij het compileren van een bestand zijn functies en variabelen die in hetzelfde bestand zijn gedefinieerd standaard niet beschikbaar tijdens macro-uitvoering. Alle definities van functies en variabelen, in hetzelfde bestand, die door een macro worden gebruikt, moeten in een EVAL-WHEN
formulier worden ingepakt. De EVAL-WHEN
moet alle drie keer zijn opgegeven, wanneer de bijgevoegde code ook moet worden geëvalueerd tijdens het laden en uitvoeren.
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun foobar () ...))
Dit is niet van toepassing op functies die worden aangeroepen vanuit de uitbreiding van de macro, alleen op functies die door de macro zelf worden aangeroepen.
Gemeenschappelijke macropatronen
TODO: Verplaats misschien de uitleg naar opmerkingen en voeg voorbeelden afzonderlijk toe
FOOF
In Common Lisp is er een concept van algemene verwijzingen . Hiermee kan een programmeur waarden instellen op verschillende "plaatsen" alsof het variabelen zijn. Macro's die van deze mogelijkheid gebruikmaken, hebben vaak een F
postfix in de naam. De plaats is meestal het eerste argument voor de macro.
Voorbeelden uit de standaard: INCF
, DECF
, ROTATEF
, SHIFTF
, REMF
.
Een gek voorbeeld, een macro die het teken van een nummeropslag op een plaats omdraait:
(defmacro flipf (place)
`(setf ,place (- ,place)))
MET-FOO
Macro's die een bron verwerven en veilig vrijgeven, worden meestal genoemd met een WITH-
-prefix. De macro moet meestal syntaxis gebruiken zoals:
(with-foo (variable details-of-the-foo...)
body...)
Voorbeelden uit de standaard: WITH-OPEN-FILE
, WITH-OPEN-STREAM
, WITH-INPUT-FROM-STRING
WITH-OUTPUT-TO-STRING
, WITH-OUTPUT-TO-STRING
.
Een benadering voor het implementeren van dit type macro dat enkele valkuilen van naamvervuiling en onbedoelde meervoudige evaluatie kan voorkomen, is door eerst een functionele versie te implementeren. De eerste stap bij het implementeren van een macro with-widget
die veilig een widget maakt en daarna opschoont, is bijvoorbeeld een functie:
(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
Omdat dit een functie is, zijn er geen zorgen over de reikwijdte van namen binnen de functie of leverancier , en het maakt het gemakkelijk om een overeenkomstige macro te schrijven:
(defmacro with-widget ((var &rest args) &body body)
`(call-with-widget (list ,@args) (lambda (,var) ,@body)))
DO-FOO
Macro's die iets herhalen worden vaak genoemd met een DO
preffix. De macrosyntaxis moet meestal de vorm hebben
(do-foo (variable the-foo-being-done return-value)
body...)
Voorbeelden uit de standaard: DOTIMES
, DOLIST
, DO-SYMBOLS
.
FOOCASE, EFOOCASE, CFOOCASE
Macro's die overeenkomen met een invoer voor bepaalde gevallen, worden vaak genoemd met een CASE
-postfix. Er is vaak een E...CASE
-variant, die een fout aangeeft als de invoer niet overeenkomt met een van de gevallen, en C...CASE
, die een continueerbare fout aangeeft. Ze moeten syntaxis hebben zoals
(foocase input
(case-to-match-against (optionally-some-params-for-the-case)
case-body-forms...)
more-cases...
[(otherwise otherwise-body)])
Voorbeelden uit de standaard: CASE
, TYPECASE
, HANDLER-CASE
.
Een macro die bijvoorbeeld een tekenreeks vergelijkt met reguliere expressies en de registergroepen aan variabelen bindt. Gebruikt CL-PPCRE voor reguliere expressies.
(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
Macro's die dingen definiëren, worden meestal DEFINE-
of DEF
-prefix genoemd.
Voorbeelden uit de standaard: DEFUN
, DEFMACRO
, DEFINE-CONDITION
.
Anaforische macro's
Een anaforische macro is een macro die een variabele (vaak IT
) introduceert die het resultaat van een door de gebruiker aangeleverd formulier vastlegt. Een veelgebruikt voorbeeld is de Anaforische If, die lijkt op een normale IF
, maar ook de variabele IT
definieert om te verwijzen naar het resultaat van het testformulier.
(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
Macro-uitbreiding is het proces waarbij macro's in daadwerkelijke code worden omgezet. Dit gebeurt meestal als onderdeel van het compilatieproces. De compiler zal alle macroformulieren uitbreiden voordat de code daadwerkelijk wordt gecompileerd. Macro-uitbreiding vindt ook plaats tijdens de interpretatie van Lisp-code.
Men kan MACROEXPAND
handmatig oproepen om te zien waar een MACROEXPAND
zich op uitbreidt.
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
is hetzelfde, maar wordt slechts eenmaal uitgebreid. Dit is handig wanneer u probeert een macrovorm te begrijpen die wordt uitgebreid naar een andere macrovorm.
CL-USER> (macroexpand-1 '(with-open-file (file "foo")
(do-something-with file)))
(WITH-OPEN-STREAM (FILE (OPEN "foo")) (DO-SOMETHING-WITH FILE))
Merk op dat noch MACROEXPAND
noch MACROEXPAND-1
de Lisp-code op alle niveaus uitbreiden. Ze breiden alleen de macro op het hoogste niveau uit. Om een formulier volledig macroexpand op alle niveaus, moet men een code wandelaar om dit te doen. Deze faciliteit is niet voorzien in de Common Lisp-standaard.
Backquote - codesjablonen schrijven voor macro's
Macro's retourcode. Omdat code in Lisp uit lijsten bestaat, kan men de reguliere lijstmanipulatiefuncties gebruiken om deze te genereren.
;; A pointless macro
(defmacro echo (form)
(list 'progn
(list 'format t "Form: ~a~%" (list 'quote form))
form))
Dit is vaak erg moeilijk te lezen, vooral in langere macro's. Met de Backquote-leesmacro kunt u geciteerde sjablonen schrijven die zijn ingevuld door elementen selectief te evalueren.
(defmacro echo (form)
`(progn
(format t "Form: ~a~%" ',form)
,form))
(macroexpand '(echo (+ 3 4)))
;=> (PROGN (FORMAT T "Form: ~a~%" '(+ 3 4)) (+ 3 4))
Deze versie lijkt bijna op gewone code. De komma's worden gebruikt om FORM
te evalueren; al het andere wordt geretourneerd zoals het is. Merk op dat in ',form
het enkele citaat buiten de komma staat, dus het zal worden geretourneerd.
Je kunt ook ,@
om een lijst in de positie te splitsen.
(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 kan ook buiten macro's worden gebruikt.
Unieke symbolen om naamconflicten in macro's te voorkomen
De uitbreiding van een macro moet vaak symbolen gebruiken die door de gebruiker niet als argumenten zijn doorgegeven (bijvoorbeeld als namen voor lokale variabelen). Men moet ervoor zorgen dat dergelijke symbolen niet kunnen conflicteren met een symbool dat de gebruiker in de omringende code gebruikt.
Dit wordt meestal bereikt met GENSYM
, een functie die een nieuw, niet-geïnterneerd symbool retourneert.
Slecht
Overweeg de onderstaande macro. Het maakt een DOTIMES
loop die ook het resultaat van de body verzamelt in een lijst, die aan het einde wordt geretourneerd.
(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)
Dit lijkt in dit geval te werken, maar als de gebruiker toevallig een variabelenaam RESULT
, die ze in de body gebruiken, zouden de resultaten waarschijnlijk niet zijn wat de gebruiker verwacht. Overweeg deze poging om een functie te schrijven die een lijst verzamelt van alle getallen tot N
:
(defun sums-upto (n)
(let ((result 0))
(dotimes+collect (i n)
(incf result i))))
(sums-upto 10) ;=> Error!
Mooi zo
Om het probleem op te lossen, moeten we GENSYM
gebruiken om een unieke naam te genereren voor de RESULT
-variabele in de macro-uitbreiding.
(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: Hoe symbolen van tekenreeksen te maken
TODO: Problemen met symbolen in verschillende pakketten vermijden
als-laat, wanneer-laat, -let macro's
Deze macro's voegen besturingsstroom en binding samen. Ze zijn een verbetering ten opzichte van anaforische anaforische macro's omdat ze de ontwikkelaar betekenis geven door middel van naamgeving. Als zodanig wordt hun gebruik aanbevolen boven hun anaforische tegenhangers.
(if-let (user (get-user user-id))
(show-dashboard user)
(redirect 'login-page))
FOO-LET
macro's binden een of meer variabelen en gebruiken die variabelen vervolgens als testformulier voor de bijbehorende voorwaardelijke ( IF
, WHEN
). Meerdere variabelen worden gecombineerd met AND
. De gekozen tak wordt uitgevoerd met de bindingen van kracht. Een eenvoudige variabele implementatie van IF-LET
kan er ongeveer zo uitzien:
(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.~%")))
Een versie die meerdere variabelen ondersteunt, is beschikbaar in de bibliotheek van Alexandria .
Macro's gebruiken om gegevensstructuren te definiëren
Een veelgebruikt gebruik van macro's is het maken van sjablonen voor gegevensstructuren die voldoen aan gemeenschappelijke regels, maar die verschillende velden kunnen bevatten. Door een macro te schrijven, kunt u de gedetailleerde configuratie van de datastructuur opgeven zonder dat u de boilerplate-code hoeft te herhalen, noch een minder efficiënte structuur (zoals een hash) in het geheugen gebruiken om de programmering te vereenvoudigen.
Stel bijvoorbeeld dat we een aantal klassen willen definiëren die een reeks verschillende eigenschappen hebben, elk met een getter en een setter. Bovendien willen we voor sommige (maar niet alle) van deze eigenschappen dat de setter een methode oproept voor het object om aan te geven dat de eigenschap is gewijzigd. Hoewel Common LISP al een afkorting heeft voor het schrijven van getters en setters, zou voor het schrijven van een standaard aangepaste setter op deze manier normaal gesproken de code moeten worden gedupliceerd die de meldingsmethode in elke setter aanroept, wat lastig kan zijn als er een groot aantal eigenschappen bij betrokken zijn . Door een macro te definiëren, wordt het echter veel eenvoudiger:
(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)))
We kunnen nu schrijven (defclass-notifier-slots foo (bar baz qux) (waldo))
en meteen een klasse foo
definiëren met een regulier slot waldo
(gemaakt door het tweede deel van de macro met de specificatie (waldo :accessor waldo)
) , en slots bar
, baz
en qux
met setters die de changed
methode aanroepen (waarbij de getter wordt gedefinieerd door het eerste deel van de macro (bar :reader bar)
en de setter door de opgeroepen notifier
).
Naast het feit dat we snel meerdere klassen kunnen definiëren die zich op deze manier gedragen, met een groot aantal eigenschappen, zonder herhaling, hebben we het gebruikelijke voordeel van hergebruik van code: als we later besluiten de manier waarop de kennisgevingsmethoden werken te wijzigen, kunnen we eenvoudig de macro en de structuur van elke klasse die deze gebruikt, zal veranderen.