Makra

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)))

Skrócona notacja do budowania list

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.

Higiena

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.

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)