Makra w Lispie służą do definiowania nowych konstrukcji syntaktycznych. Mają do dyspozycji pełną moc języka. Choć definicja makra przypomina definicję funkcji, należy być ostrożnym. Na przykład często makra używane podczas kompilacji mogą nie być dostępne podczas wykonania. Nie mozna też używać ich jako paramatrów funkcyjnych.
Makr nie należy używać do wklejania treści drobnych funkcji w kod.
Do tego służy inline
.
Argumenty (należałoby raczej powiedzieć składowe) makra nie są obliczane przed wejściem w treść. Treść makra powinna zwrócić wyrażenie, które będzie umieszczone w kodzie zamiast makrowołania.
Na początek coś znanego
(defmacro my-if (warunek gdy-tak &optional gdy-nie) (list 'cond (list warunek gdy-tak) (list t gdy-nie)))
Można czytelniej
(defmacro my-if (warunek gdy-tak &optional gdy-nie) `(cond (,warunek ,gdy-tak) (t ,gdy-nie)))
Dotyczy przede wszystkim makr, ale przydaje się również w innych miejscach. Często konstruujemy listy, których zawartość w większej części jest ustalona, ale tu i tam są wyrazenia Lispu. Najbardziej dotkliwe jest to w makrach, bo tam budujemy wyrażenia stanowiące makro-rozwinięcie.
Powiedzmy, że postanowiliśmy zrealizować konstrukcję while
(while warunek wyrażenie ...)
Definiujemy więc makro
(defmacro while (warunek &rest wyrazenia) (append (list 'loop 'while warunek 'do) wyrazenia))
Znacznie przyjemniej jest napisać
(defmacro while (warunek &rest wyrazenia) `(loop while ,warunek do ,@wyrazenia))i lepiej się to czyta.
Odwrócony apostrof (backquote) działa jak quote
,
ale znajdujące się wewnątrz elementy poprzedzone przecinkiem są
obliczane i zastępowane swoją wartością. Jeśli po przecinku był
znak `@', to wartość powinna być listą i jej elementy są wplatane
w otaczającą listę. System sam decyduje, jak rozwinąć backquote.
Może to być tak jak u góry, a może też być
(cons 'while (cons warunek (cons 'do wyrazenia)))
Inny przykład
(defmacro rule (trigger &rest body) `(add-rule ',trigger ',body))Zastępujemy makrowołanie wywołaniem funkcji, makro tylko poprzedza apostrofem argumenty.
Listy parametrów dla makra mogą być wielopoziomowe, z zagnieżdżonymi podlistami. Jest to przecież mechanizm do definiowania rozszerzeń syntaktycznych. Bez tego trudno by się definiowało takie makra jak with-open-file.
Na koniec ciekawostka: można definiować makra lokalnie przez
(macrolet (Każda definicja jest podobna do flet lub labels:...) ...)
(ale opisuje lokalne makro....)
O co chodzi z tą higiena? Popatrzmy
* (defmacro repeat (akcja ile-razy) `(loop for i from 1 to ,ile-razy do ,akcja)) REPEAT * (repeat (print '*) 5) * * * * * NIL
Niby dobrze. Ale
* (let ((i '*)) (repeat (print i) 5)) ; in: LET ((I '*)) ; (LET ((I '*)) ; (REPEAT (PRINT I) 5)) ; ; caught STYLE-WARNING: ; The variable I is defined but never used. ; ; compilation unit finished ; caught 1 STYLE-WARNING condition 1 2 3 4 5 NIL
Oj, nieładnie. Coś ukradło gwiazdki. Przydałoby się więc trochę higieny.
Popatrzmy na ciekawy komunikat SBCL. Uważa, że nie używamy zmiennej
i
. Pora na macroexpand
.
* (macroexpand-1 '(repeat (print i) 5)) (LOOP FOR I FROM 1 TO 5 DO (PRINT I)) T * (macroexpand- '(repeat (print i) 5)) (BLOCK NIL (LET ((I 1)) (DECLARE (TYPE (AND REAL NUMBER) I)) (TAGBODY SB-LOOP::NEXT-LOOP (WHEN (> I '5) (GO SB-LOOP::END-LOOP)) (PRINT I) (SB-LOOP::LOOP-DESETQ I (1+ I)) (GO SB-LOOP::NEXT-LOOP) SB-LOOP::END-LOOP))) T
W treści makra użyliśmy zmiennej lokalnej i
,
która przesłoniła zewnętrzną. Co z tym zrobić? Pora na symbole unikalne.
Przy rozwijaniu makr przydają się unikalne identyfikatory, których nikt inny nie ma i miec nie może.
Funkcja gensym
zwraca nowy symbol, o w miarę unikalnie
zbudowanej nazwie, bez umieszczania go w tablicy symboli jakiegokolwiek
pakietu (inaczej mówiąc, nie internuje go). Taki swobodny symbol
jest wypisywany z prefiksem ``#:''
* (gensym "ala") #:|ala439| * (gensym "ALA") #:ALA440 * (equal '#:ala '#:ala) NIL * (equal 'ala 'ala) T
Funkcja gensym
używa zmiennej dynamicznej
*gensym-counter*
. Tworząc nowe wiązanie tej zmiennej można
uniknąć podawania argumentu opcjonalnego (podobno zalecane).
No to wracamy do definicji repeat
* (defmacro repeat (akcja ile-razy) (let ((i (gensym))) `(loop for ,i from 1 to ,ile-razy do ,akcja))) WARNING: redefining COMMON-LISP-USER::REPEAT in DEFMACRO REPEAT * (let ((i '*)) (repeat (print i) 5)) * * * * * NIL
Duuużo lepiej. Ale można inaczej (zagadka!)
(defmacro repeat (akcja ile-razy) `(let ((foo (lambda () ,akcja))) (loop for i from 1 to ,ile-razy do (funcall foo))))
Makro defsetf
służy do definiowania funkcji modyfikującej
dla zmiennych uogólnionych postaci (access-fn ...).
(defsetf access-fn update-fn [dokumentacja]) (defsetf access-fn lista-parametrów (store-variable) {deklaracja | dokumentacja} ... wyrażenie ...)
Parametr access-fn musi być symbolem nazywającym zwykłą funkcję, tzn. obliczającą wszystkie swoje argumenty.
W wariancie prostszym podajemy funkcję update-fn, mającą jeden parametr więcej niż access-fn: nową wartość. Pozostałe parametry są takie same. Funkcja update-fn zwraca nową wartość, uprzednio umieszczając ją w miejscu wskazanym przez access-fn.
Wariant złożony odpowiada definicji makra z dodatkową zmienną związaną z nową wartością. Wymagania takie same jak w wariancie prostszym.
Przykłady
;; Jeszcze jeden synonim. * (defmacro tail (l) `(cdr ,l)) TAIL ;; Aby móc zmieniać destrukcyjnie ogon, trzeba zdefiniować metodę SETF. * (defsetf tail (l) (new-tail) `(progn (rplacd ,l ,new-tail) ,new-tail)) TAIL ;; No to potestujmy * (defparameter my-list '(a b c)) (A B C) * (tail my-list) (B C) * (setf (tail my-list) '(y z)) (Y Z) ;; Działa. Naprawdę... * my-list (A Y Z) ;; To samo dla CAR, ale z funkcją SET-HEAD * (defmacro head (l) `(car ,l)) * (defun set-head (l new-head) (rplaca l new-head)) * (defsetf head set-head) ;; Test * (setf my-lis '(a b c)) (A B C) * (head my-lis) A * (setf (head my-lis) 'z) (Z B C) * my-lis (Z B C)