Makrá v Lispe sú zložitejšie a majú omnoho viac možností než napríklad preprocesorové makrá jazyka C. Tieto možnosti ale prinášajú aj možné problémy, ktoré treba mať na pamäti a vyhnúť sa im. Prvá vec je, že generujú kód ešte v čase kompilácie – to znamená, že niekde ich generácia musí končiť, nesmú byť rekurzívne, takéto makro sa neskompiluje.
Na pomoc pri vývoji máme k dispozícii macroexpand
a macroexpand-1
,
resp. v prostredí Emacsu a SLIME s predponou slime-
(M-x názov;
skratka C-c RET
). macroexpand-1
vyhodnotí makro jedenkrát, už
nevyhodnocuje ďalej ani keď je výsledok ďalšie volanie makra.
Pozrime sa ako sa expanduje makro, ktorým sme si počítali {π}; v druhej časti seriálu:
(defmacro pi-macro (n) `(+ ,@(mapcar #'(lambda (n) (if (= n 0) 1 (/ (if (evenp n) 1 -1) (+ 1 (* n 2))))) (loop for x from 0 to n collect x)))) ;; expandujeme volanie makra (pi-macro 10) ;; výsledok (+ 1 -1/3 1/5 -1/7 1/9 -1/11 1/13 -1/15 1/17 -1/19 1/21)
Niektorých zrejme napadne, že na tomto makre nie je niečo v poriadku.
Spomínal som, že makro sa vyhodnocuje v čase kompilácie, pre priamo
zadané číslo to funguje, ale ak by sme chceli zadať premennú tak
neuspejeme. Volanie mapcar
sa totiž vyhodnocuje v čase kompilácie a
premenná nadobúda hodnotu v čase behu. Tento rozdiel nám umožňuje
vytvárať makrá typu loop
, ktoré prijíma rôzne kľúčové slová (má
vlastnú špecifickú syntax) a podľa nich vygeneruje kód.
Hlavné použitie makier spočíva v tom, že za nás dokážu generovať
opakujúci sa kód, ale hlavne taký, ktorý nie je možné jednoducho
zapísať funkciou. Makrá sú použité aj v štandardnej knižnici, kde
napríklad do
a loop
generujú dosť nízkoúrovňový kód, ktorý môže
byť problematické čítať (alebo dokonca expandujú na kód, ktorý je
implementačne závislý).
;; dotimes je makro, ktoré využíva makro "do" na zjednodušenie inak ;; opakujúceho a menej prehľadného zápisu (dotimes (x 10) (format t "hello world!")) ;; slime-macroexpand-1 (DO ((X 0 (1+ X))) ((>= X 10) NIL) (DECLARE (TYPE UNSIGNED-BYTE X)) (FORMAT T "hello world!"))
Ďalšia expanzia je už veľmi nízkoúrovňová a neprehľadná.
Makro svoje parametre nevyhodnocuje, to je tiež dôležitá vlastnosť,
inak by vyššie expandované makro do
nemohlo napríklad obsahovať
zoznam (X 0 (1+ X))
, pretože symbol X
by musel byť funkcia alebo
makro.
S týmto poznatkom ľahko zistíme ďalšiu vec, na ktorú treba pamätať: každý argument vyhodnocovať len jedenkrát. Pokiaľ by sme použili argument dva alebo viackrát, mohla by nastať podobná situácia ako v nasledujúcom príklade:
;; zlé makro (defmacro sucet (n) `(+ ,n ,n)) ;; zavoláme ho s nepredpokladaným argumentom (sucet (random 10)) ;; argument sa nevyhodnocuje, preto je výsledok súčet náhodných čísel (+ (RANDOM 10) (RANDOM 10))
Takže makro skúsime opraviť:
(defmacro sucet (n) `(let ((x ,n)) (+ x x))) ;; nová expanzia (LET ((X (RANDOM 10))) (+ X X))
Toto už je korektné, no spravme si ďalšie nezmyselné makro, ktoré je ale účelovo trošku zložitejšie:
(defmacro sucet (m n) `(let ((x ,n)) (+ ,m x x))) ;; expandujeme v takomto prostredí (let ((x 10)) (sucet x (random 10))) ;; vo výsledku pôvodné x nefiguruje a súčet teda môže byť aj menší než 10 (LET ((X (RANDOM 10))) (+ X X X))
Na vyriešenie tohoto problému potrebujeme naozaj náhodné meno
premennej, to dostaneme zavolaním gensym
:
;; toto makro si vygeneruje unikátny symbol (defmacro sucet (m n) (let ((x (gensym))) `(let ((,x ,n)) (+ ,m ,x ,x)))) ;; a ten sa vo vyššie uvedenom prostredí naozaj dosadí za x (LET ((#:G1724 (RANDOM 10))) (+ X #:G1724 #:G1724))
Toto je tak časté, že sa všeobecne používa makro, ktoré toto spraví za nás:
(defmacro with-gensyms ((&rest names) &body body) `(let ,(loop for n in names collect `(,n (gensym))) ,@body)) ;; nová verzia (defmacro sucet (m n) (with-gensyms (x) `(let ((,x ,n)) (+ ,m ,x ,x)))))
Makrá dokážu rozkladať parametre, preto makro with-gensyms
môže
prijať zoznam names
takýmto spôsobom. &body
v makre je
zameniteľné s &rest
, ďalej makro môže tiež prijímať rovnaké typy
argumentov ako funkcia. Pridaný typ je &whole
, ktorý uloží celé
volanie makra do premennej a za ním môže nasledovať obyčajný zoznam
argumentov.
Ďalšia dôležitá vec je poradie vyhodnocovania vo výslednom kóde – pokiaľ nie je dôvod robiť inak, vyhodnocujeme rovnakom poradí ako sú zadané (v súlade so zbytkom jazyka).
;; nesprávne poradie produkuje nepredpokladané výsledky (defmacro sucet (x y) `(+ ,y ,x)) ;; očakávame súčet 6 a 5 (let ((a 5)) (sucet (incf a) (decf a))) ;; a dostaneme pravý opak (+ (DECF A) (INCF A)) ;; čo je samozrejme 9
Makro kombinujúce generovanie symbolov, vyhodnotenie jedenkrát a v
prípade správneho uvedenia argumentov aj vyhodnocovanie zľava doprava
je once-only
(obsahuje ho napr. balíček cl-utilities
):
(defmacro once-only ((&rest names) &body body) (let ((gensyms (loop for n in names collect (gensym)))) `(let (,@(loop for g in gensyms collect `(,g (gensym)))) `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n))) ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g))) ,@body)))))
Toto makro je trochu ťažšie pochopiteľné, v skratke: vytvorí si sadu symbolov, do ktorých si vygeneruje symboly, ktoré budú použité na uloženie hodnôt premenných a nakoniec priradí menám premenných symboly obsahujúce ich pôvodné hodnoty (a preto ich mená môžeme použiť v tele makra).
To je pre túto časť všetko, nabudúce si stručne povieme v čom sú OOP a výnimky v Common Lispe odlišné od tradičnejších jazykov.
Pre pridávanie komentárov sa musíte prihlásiť.