Hlavní navigace

LISPová makra aneb programovatelný programovací jazyk

Daniel Novotný

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.

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.

Našli jste v článku chybu?

7. 8. 2007 14:08

"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."

To je trosku LISP-centricke :-) I jine jazyky nabizi podobnou rozsiritelnost, i kdyz je pravda, ze nejde zrovna o jazyky mainstreamove (mezi ty radim Javu, C, C++, C#, PHP atd.). Treba takovy FORTH a vlastne i Tcl jde podobnym zpusobem rozsirovat o dalsi syntakticke prvky.

Ale zamysleme se nad tim, proc to je vlastne mozne? Tyto j…



21. 8. 2007 0:48

disorder (neregistrovaný)
aj mne sa paci, nejaky cas ju uz mam rozcitanu :/
Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu

Lupa.cz: Brněnský radní chce zničit kartel operátorů. Uspěje?

Brněnský radní chce zničit kartel operátorů. Uspěje?

Vitalia.cz: Taky věříte na pravidlo 5 sekund?

Taky věříte na pravidlo 5 sekund?

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

120na80.cz: Pánové, pečujte o svoje přirození a prostatu

Pánové, pečujte o svoje přirození a prostatu

Měšec.cz: mBank cenzuruje, zrušila mFórum

mBank cenzuruje, zrušila mFórum

Podnikatel.cz: Chtějte údaje k dani z nemovitostí do mailu

Chtějte údaje k dani z nemovitostí do mailu

Lupa.cz: Propustili je z Avastu, už po nich sahá ESET

Propustili je z Avastu, už po nich sahá ESET

Vitalia.cz: 9 největších mýtů o mase

9 největších mýtů o mase

Lupa.cz: Teletext je „internetem hipsterů“

Teletext je „internetem hipsterů“

Měšec.cz: Kdy vám stát dá na stěhování 50 000 Kč?

Kdy vám stát dá na stěhování 50 000 Kč?

120na80.cz: Co všechno ovlivňuje ženskou plodnost?

Co všechno ovlivňuje ženskou plodnost?

DigiZone.cz: ČRa DVB-T2 ověřeno: Hisense a Sencor

ČRa DVB-T2 ověřeno: Hisense a Sencor

Podnikatel.cz: K EET. Štamgast už peníze na stole nenechá

K EET. Štamgast už peníze na stole nenechá

120na80.cz: Na ucho teplý, nebo studený obklad?

Na ucho teplý, nebo studený obklad?

Lupa.cz: Proč firmy málo chrání data? Chovají se logicky

Proč firmy málo chrání data? Chovají se logicky

Vitalia.cz: Proč vás každý zubař posílá na dentální hygienu

Proč vás každý zubař posílá na dentální hygienu

Měšec.cz: U levneELEKTRO.cz už reklamaci nevyřídíte

U levneELEKTRO.cz už reklamaci nevyřídíte

Měšec.cz: Air Bank zruší TOP3 garanci a zdražuje kurzy

Air Bank zruší TOP3 garanci a zdražuje kurzy