common-lisp
makron
Sök…
Anmärkningar
Makroens syfte
Makron är avsedda för att generera kod, omvandla kod och tillhandahålla nya notationer. Dessa nya notationer kan vara mer lämpade för att bättre uttrycka programmet, till exempel genom att tillhandahålla domännivåkonstruktioner eller hela nya inbäddade språk.
Makron kan göra källkoden mer självförklarande, men felsökning kan göras svårare. Som tumregel bör man inte använda makron när en vanlig funktion kommer att göra. När du använder dem, undvik de vanliga fallgroparna, försök att hålla fast vid de vanligt förekommande mönstren och namnge konventionerna.
Makroekspansion beställning
Jämfört med funktioner expanderas makron i omvänd ordning; yttersta först, inmost sist. Detta innebär att man som standard inte kan använda ett inre makro för att generera syntax som krävs för ett yttre makro.
Utvärderingsorder
Ibland behöver makron flytta runt användarformulär. Man måste se till att inte ändra ordningen i vilken de utvärderas. Användaren kan lita på att biverkningar sker i sin ordning.
Utvärdera endast en gång
Utvidgningen av ett makro behöver ofta använda värdet på samma användarformulär form mer än en gång. Det är möjligt att formuläret råkar ha biverkningar, eller att det kanske kallar en dyr funktion. Således måste makroen se till att bara utvärdera sådana former en gång. Vanligtvis görs detta genom att tilldela värdet till en lokal variabel (vars namn är GENSYM
ed).
Funktioner som används av makron, med EVAL-WHEN
Komplexa makron har ofta delar av sin logik implementerade i separata funktioner. Man måste dock komma ihåg att makron utvidgas innan den faktiska koden sammanställs. När du sammanställer en fil kommer funktionalitet och variabler som definieras i samma fil som standard inte att finnas tillgängliga under makroekvertering. Alla funktions- och variabeldefinitioner, i samma fil som används av ett makro, måste lindas in i en EVAL-WHEN
form. EVAL-WHEN
ska ha alla tre gånger angivna, när den bifogade koden också ska utvärderas under belastning och körtid.
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun foobar () ...))
Detta gäller inte för funktioner som kallas från makroutvidgningen, bara de som anropas av makroen själv.
Vanliga makromönster
TODO: Flytta kanske förklaringarna till kommentarer och lägg till exempel separat
foof
I Common Lisp finns det ett koncept med generaliserade referenser . De tillåter en programmerare att ställa in värden till olika "platser" som om de var variabler. Makron som använder denna förmåga har ofta ett F
postfix i namnet. Platsen är vanligtvis det första argumentet till makroen.
Exempel från standarden: INCF
, DECF
, ROTATEF
, SHIFTF
, REMF
.
Ett dumt exempel, ett makro som vänder tecknet på ett nummerlager på en plats:
(defmacro flipf (place)
`(setf ,place (- ,place)))
MED-FOO
Makron som skaffar och släpper en resurs namnges vanligtvis med ett WITH-
prefix. Makroen bör vanligtvis använda syntax som:
(with-foo (variable details-of-the-foo...)
body...)
Exempel från standarden: WITH-OPEN-FILE
, WITH-OPEN-STREAM
, WITH-INPUT-FROM-STRING
, WITH-OUTPUT-TO-STRING
.
En metod för att implementera denna typ av makro som kan undvika några av fallgroparna med namnföroreningar och oavsiktlig multipelutvärdering är att implementera en funktionell version först. Till exempel kan det första steget i att implementera ett makro with-widget
som säkert skapar en widget och rensas upp efteråt vara en funktion:
(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
Eftersom detta är en funktion, finns det inga problem med namnet inom funktion eller leverantör , och det gör det enkelt att skriva ett motsvarande makro:
(defmacro with-widget ((var &rest args) &body body)
`(call-with-widget (list ,@args) (lambda (,var) ,@body)))
DO-FOO
Makroer som itererar över något namnges ofta med ett DO
föredrag. Makrosyntaxen bör vanligtvis vara i form
(do-foo (variable the-foo-being-done return-value)
body...)
Exempel från standarden: DOTIMES
, DOLIST
, DO-SYMBOLS
.
FOOCASE, EFOOCASE, CFOOCASE
Makron som matchar en inmatning mot vissa fall benämns ofta med ett CASE
postfix. Det finns ofta en E...CASE
-variant, som signalerar ett fel om ingången inte matchar något av fallen, och C...CASE
, som signalerar ett kontinuerligt fel. De borde ha syntax som
(foocase input
(case-to-match-against (optionally-some-params-for-the-case)
case-body-forms...)
more-cases...
[(otherwise otherwise-body)])
Exempel från standarden: CASE
, TYPECASE
, HANDLER-CASE
.
Till exempel ett makro som matchar en sträng mot reguljära uttryck och binder registergrupperna till variabler. Använder CL-PPCRE för vanliga uttryck.
(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
Makron som definierar saker benämns vanligtvis antingen med DEFINE-
eller DEF
förnyelse.
Exempel från standarden: DEFUN
, DEFMACRO
, DEFINE-CONDITION
.
Anaforiska makron
En anaforisk makro är ett makro som introducerar en variabel (ofta IT
) som fångar resultatet av ett användarlevererat formulär. Ett vanligt exempel är den Anaforiska If, som är som en vanlig IF
, men också definierar variabeln IT
att referera till resultatet av testformen.
(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
Makroutvidgning är processen att förvandla makron till faktisk kod. Detta händer vanligtvis som en del av sammanställningsprocessen. Kompilatorn utvidgar alla makroformer innan den faktiskt sammanställer kod. Makroutvidgning sker också under tolkning av Lisp-kod.
Man kan ringa MACROEXPAND
manuellt för att se vad en makroform expanderar till.
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
är densamma, men expanderar bara en gång. Det här är användbart när man försöker få mening om en makroform som expanderar till en annan makroform.
CL-USER> (macroexpand-1 '(with-open-file (file "foo")
(do-something-with file)))
(WITH-OPEN-STREAM (FILE (OPEN "foo")) (DO-SOMETHING-WITH FILE))
Observera att varken MACROEXPAND
eller MACROEXPAND-1
utökar Lisp-koden på alla nivåer. De utökar bara makroformen på toppnivå. För att makroexpandera ett formulär helt på alla nivåer behöver man en kodvalerare för att göra det. Denna anläggning tillhandahålls inte i Common Lisp-standarden.
Backquote - skriva kodmallar för makron
Makro returkod. Eftersom kod i Lisp består av listor kan man använda de vanliga listmanipuleringsfunktionerna för att generera den.
;; A pointless macro
(defmacro echo (form)
(list 'progn
(list 'format t "Form: ~a~%" (list 'quote form))
form))
Detta är ofta mycket svårt att läsa, särskilt i längre makron. Med Backquote-läsarmakroet kan man skriva citerade mallar som fylls i genom att utvärdera element selektivt.
(defmacro echo (form)
`(progn
(format t "Form: ~a~%" ',form)
,form))
(macroexpand '(echo (+ 3 4)))
;=> (PROGN (FORMAT T "Form: ~a~%" '(+ 3 4)) (+ 3 4))
Denna version ser nästan ut som vanlig kod. Kommorna används för att utvärdera FORM
; allt annat returneras som det är. Lägg märke till att i ',form
det enda offertet ligger utanför kommaet, så kommer det att returneras.
Man kan också använda ,@
att dela en lista i positionen.
(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 också användas utanför makron.
Unika symboler för att förhindra namnkollisioner i makron
Utvidgningen av ett makro behöver ofta använda symboler som inte har gått som argument av användaren (som namn på exempelvis lokala variabler). Man måste se till att sådana symboler inte kan komma i konflikt med en symbol som användaren använder i den omgivande koden.
Detta uppnås vanligtvis med GENSYM
, en funktion som returnerar en ny ointern symbol.
Dålig
Tänk på makroen nedan. Det gör en DOTIMES
slinga som också samlar kroppens resultat till en lista som returneras i slutet.
(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)
Detta verkar fungera i det här fallet, men om användaren råkade ha ett variabelt namn RESULT
, som de använder i kroppen, skulle resultaten förmodligen inte vara vad användaren förväntar sig. Tänk på detta försök att skriva en funktion som samlar en lista med summor av alla heltal upp till N
:
(defun sums-upto (n)
(let ((result 0))
(dotimes+collect (i n)
(incf result i))))
(sums-upto 10) ;=> Error!
Bra
För att lösa problemet måste vi använda GENSYM
att generera ett unikt namn för RESULT
-variabel i makroutvidgningen.
(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: Hur man gör symboler från strängar
TODO: Undvik problem med symboler i olika paket
if-let, When-let, -lilla makron
Dessa makroer slår samman kontrollflöde och bindning. De är en förbättring jämfört med anaforiska anaforiska makron eftersom de låter utvecklaren kommunicera mening genom namngivning. Som sådan rekommenderas deras användning jämfört med deras anaforiska motsvarigheter.
(if-let (user (get-user user-id))
(show-dashboard user)
(redirect 'login-page))
FOO-LET
makroer binder en eller flera variabler och använder sedan dessa variabler som testform för motsvarande villkorade ( IF
, WHEN
). Flera variabler kombineras med AND
. Den valda grenen utförs med bindningarna i kraft. En enkel variabel implementering av IF-LET
kan se ut som:
(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.~%")))
En version som stöder flera variabler finns i Alexandria- biblioteket.
Använda makron för att definiera datastrukturer
En vanlig användning av makron är att skapa mallar för datastrukturer som följer vanliga regler men som kan innehålla olika fält. Genom att skriva ett makro kan du tillåta att den detaljerade konfigurationen av datastrukturen specificeras utan att behöva upprepa pannplattkoden och inte heller använda en mindre effektiv struktur (till exempel en hash) i minnet för att förenkla programmeringen.
Antag till exempel att vi vill definiera ett antal klasser som har en rad olika egenskaper, var och en med en getter och setter. För vissa (men inte alla) av dessa egenskaper vill vi dessutom att setaren kallar en metod på objektet som meddelar att fastigheten har ändrats. Även om Common LISP redan har en korthet för att skriva brev och setters, kan det att skriva en standardanpassad setter på detta sätt normalt kräva duplicering av koden som kallar anmälningsmetoden i varje setter, vilket kan vara ont om det finns ett stort antal egenskaper inblandade . Men genom att definiera ett makro blir det mycket lättare:
(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)))
Vi kan nu skriva (defclass-notifier-slots foo (bar baz qux) (waldo))
och omedelbart definiera en klass foo
med en vanlig slot waldo
(skapad av den andra delen av makro med specifikationen (waldo :accessor waldo)
) , och slots bar
, baz
och qux
med inställare som kallar den changed
metoden (där getter definieras av makroens första del, (bar :reader bar)
, och setaren av den åberopade notifier
).
Förutom att vi snabbt kan definiera flera klasser som uppför sig på detta sätt, med ett stort antal egenskaper utan upprepning, har vi den vanliga fördelen med kodanvändning: om vi senare beslutar att ändra hur anmälningsmetoderna fungerar, kan vi helt enkelt ändra makro, och strukturen för varje klass som använder den kommer att förändras.