LISPová makra aneb programovatelný programovací jazyk

7. 8. 2007
Doba čtení: 5 minut

Sdílet

LISP je velmi mocný programovací jazyk, disponující důležitou vlastností, která jej odlišuje a vyzdvihuje nad ostatní jazyky. Tou vlastností jsou LISPová makra. Jak ve skutečnosti vypadají a co dokáží? Jak se liší od maker například v C? V článku si odpovíme na tyto a další otázky a předvedeme si i praktické ukázky.

Před nějakou dobou jsem na tomto serveru publikoval článek o programovacím jazyce LISP. Nyní jsem si uvědomil, že jsem v něm nepopsal stěžejní vlastnost, která vyzdvihuje LISP nad ostatní programovací jazyky: LISPová makra. Tento článek navazuje na můj předchozí, pokud tedy nebudete vědět „která bije“, přečtěte si nejprve můj úvod do LISPu. Stejně jako minule budu používat Common Lisp.

První věc, kterou bych chtěl vyjasnit, je samotný pojem „makro“: valná většina lidí si pod pojmem „makro“ v souvislosti s programováním vybaví makra preprocesoru jazyka C. LISPová makra nemají s těmito makry nic společného: fungují úplně jinak. Menší část lidí si představí makra textového editoru: ani tady není představa přiléhavá LISPovým makrům. Žádám vás tedy, abyste „osvobodili svoji mysl“ a pojmu „makro“ v tomto článku nedávali konotace Céčkového či preprocesorové­ho makra.

Jednoduchá definice LISPového makra je následující: makro je konstrukce, které předáme (jako argumenty) nevyhodnocované s-výrazy a ono z nich sestaví vlastní LISPový kód (přitom může používat všechny funkce LISPu, i námi definované) a tento kód potom zavolá na místě, kde je zapsáno. Definuje se pomocí formy DEFMACRO, která „vypadá“ jako DEFUN.

Hmmm… ale co si pod tím představit? Nejjednodušší příklad, na kterém „vám to dojde“, je následující:

[1]>(defmacro pozadu (x) (reverse x) )
POZADU
[2]>(pozadu (1 4 -) )
3

Makro POZADU nejdřív otočilo námi zadaný s-výraz a pak ho teprve spustilo: nadefinovali jsme si tedy vlastní syntaktickou konstrukci, v podstatě vlastní jazyk nad LISPem, který akceptuje LISPové výrazy zapsané obráceně. Všimněte si, že (1 4 -) je sice platný s-výraz, ale není platná LISPová forma (tedy s-výraz, který se dá přímo vyhodnotit) – a přesto jej můžeme, neuvozený apostrofem, napsat, protože makro své argumenty nevyhodnocuje.

Na sestavení výsledného kódu „uvnitř“ makra můžeme samozřejmě použít všechny „list-sestavující“ funkce jako LIST, CONS a podobně, ale jazyk nám nabízí výhodnou syntaktickou zkratku. Je jí konstrukce se zpětným apostrofem (backquote). Tato konstrukce je ve skutečnosti jistým druhem makra, což nám ukazuje, jak jsou makra užitečná. K věci: seznam uvozený zpětným apostrofem se nevyhodnocuje, až na ty jeho položky, které jsou uvozeny čárkou. Pomocí „,@“ vyhodnotíme položku tak, že její obsah se vnoří do původního okolo stojícího seznamu.

Příklad:

[3]> (setq a 1 b 2 c '(x y z) )
(X Y Z)
[4]> `(a b c)
(A B C)
[5]> `(a ,b c)
(A 2 C)
[6]> `(,a b ,c)
(1 B (X Y Z))
[7]> `(,a b ,@c)
(1 B X Y Z)

Takže to by byla základní teorie, nyní přistoupíme k prvnímu příkladu: standard LISPu definuje speciální formu IF tak, že bere podmínku, výraz, který se vyhodnotí, když je pravdivá a druhý, který se vyhodnotí, když je nepravdivá. Často však chceme provést, pokud byla podmínka pravdivá, více výrazů. Museli bychom použít speciální formu PROGN, která nám tyto výrazy „obalí“ tak, že se IFu budou jevit jako jeden výraz: (if podminka (progn (udelej-tohle) (a-tamto) (jeste-toto))). Protože stále opakovat idiom IF PROGN může být úmorné, definuje Common Lisp makro WHEN, které se nám expanduje do IF PROGN – takže můžeme psát (when podminka (udelej-tohle) (a-tamto) (jeste-toto)). Pokud by WHEN v jazyce nebylo, mohli bychom si ho sami definovat:

(defmacro my-when (podminka &rest telo)
`(if ,podminka (progn ,@telo)))

Abychom nedefinovali pouze to, co v LISPu už je, ukážu vám, jak si nadefinovat klasický cyklus WHILE, který ve standardu Common Lispu není:

(defmacro while (podminka &rest telo)
`(tagbody
    looping
      (if (not ,podminka) (go konec) )
       ,@telo
      (go looping)
     konec))

Pokud v nějakém jiném jazyce chceme syntaxi, která tam není, musíme prosit tvůrce jazyka. Ne tak v LISPu: tam si můžeme takovou syntaxi nadefinovat sami.

Uvědomme si, že takovou syntaktickou konstrukcí, kterou můžeme makrem abstrahovat, je třeba i definice funkce. Můžeme tedy nadefinovat makro, které pokud se spustí, nám nadefinuje jistý druh funkce.

Nadefinujeme makro DEF-SHELL-FUNC, kterému předáme název funkce a řetězce a ono nám nadefinuje funkci, která pokud ji pustíme, spustí tyto řetězce jako příkazy UNIXového shellu. Příklad je víceméně pouze ilustrační, ale i taková věc se může někdy hodit:

(defmacro def-shell-func (jmeno &rest prikazy)
    (setf cele-prikazy nil)
    (dolist (p prikazy)
      (setf cele-prikazy (append cele-prikazy (list `(run-shell-command ,p)))))
    `(defun ,jmeno ()
        ,@cele-prikazy ))

potom

(def-shell-func compile-src
    "cd src"
    "make all"
    "cd .."
    "echo hotovo")

nám nadefinuje funkci COMPILE-SRC, která při svém spuštění provede zadané shellové příkazy.

Programování maker má některá úskalí – jednoduchý příklad: známe již speciální formu PROGN, která spustí formy a vrátí výsledek poslední z nich. Existuje též makro PROG1, které se chová stejně, ale vrátí hodnotu první ze zadaných forem. Pokud by náš LISP makro PROG1 neobsahoval, mohli bychom zkusit jej nadefinovat sami. Myšlenka je jednoduchá: hodnotu první formy někam uložíme a na konci vrátíme:

(defmacro my-prog1 (prvni &rest ostatni)
   `(progn
        (setq nekam ,prvni)
        ,@ostatni
        nekam
     ))

Ve velké, velmi velké většině případů bude makro fungovat, ovšem pokud někoho napadne použít proměnnou se jménem NEKAM – dle Murphyho zákona to někdo určitě udělá -, dopadne to takhle:

[16]> (my-prog1 1 2 3)
1
[17]> (my-prog1 1 2 (setq nekam 10) 3)
10

Používat velmi obskurní názvy proměnných je řešení velmi „špinavé“ a nesystémové, navíc jméno proměnné by mělo označovat její účel a ne být divné a kryptické. Musíme si uvědomit, že jména takovýchto proměnných se nesmí objevit ve vygenerovaném kódu, použití jakýchkoli proměnných v samotném makru („venku“) nevadí. Tady nám pomůže funkce GENSYM, která vygeneruje symbol, u něhož je zaručeno, že není použitý. GENSYM se pustí v okamžiku expanze makra, a použije se tedy jméno, které v okamžiku expanze nikdo nepoužívá. Viz příklad správného my-prog1

(defmacro my-prog1 (prvni &rest ostatni)
   (let ((nekam (gensym)))
   `(progn
        (setq ,nekam ,prvni)
        ,@ostatni
        ,nekam
     )))

Nyní se nám „,nekam“ expanduje na námi vytvořené jméno proměnné, které bude v těle makra… Pro zajímavost: viděl jsem v jedné knížce definované makro WITH-GENSYMS, které nám zvládne abstrahovat konstrukce vyžadující GENSYM, podobné jako v nové definici MY-PROG1.

Dále si musíme dát pozor, aby se nám některá forma nevyhodnocovala vícekrát než uživatel makra očekává: to se stává, pokud v expandovaném kódu sestavíme ten samý výraz na více místech.

bitcoin_skoleni

Pokud si nejste jisti, jak se makro expanduje, použijte funkci MACROEXPAND-1, které zadáte formu (quotovanou, samozřejmě) a ona nám ji rozexpanduje. Ta „1“ je tam proto, že funkce „MACROEXPAND“ bez jedničky expanduje makro „až na doraz“, tedy do největší hloubky tak, aby zůstaly pouze funkce a speciální formy.

To je vše – doufám, že si z mého článku vezmete inspiraci, jak lépe programovat.