Programovací jazyk Clojure 16: složitější uživatelská makra

25. 9. 2012
Doba čtení: 23 minut

Sdílet

Dnes budeme společně pokračovat v popisu makrosystému, který tvoří nedílnou součást jazyka Clojure. Nejprve vylepšíme trasovací makro navržené minule a posléze si ukážeme tvorbu složitějšího makra, díky němuž do jazyka Clojure přidáme nový typ řídicí struktury – programové smyčky.

Obsah

1. Programovací jazyk Clojure 16: složitější uživatelská makra

2. Zápis kódu generovaného makrem s využitím „šablony“

3. Příprava na vylepšení makra trace

4. Funkce s proměnným počtem parametrů

5. Příklad funkce s proměnným počtem parametrů

6. Vylepšené makro trace

7. Vytvoření nového typu programové smyčky forloop s využitím maker

8. Úprava programové smyčky forloop

9. Odkazy na Internetu

1. Programovací jazyk Clojure 16: složitější uživatelská makra

V dnešní části seriálu o programovacím jazyce Java i o virtuálním stroji Javy (JVM) se budeme podrobněji zabývat popisem makrosystému jazyka Clojure postaveného nad JVM. Minule jsme si řekli, že makra v Clojure jsou aplikována na zadávané či načítané formy ve chvíli načítání těchto forem a nikoli až v čase běhu programu – makra zde tedy slouží ke stejnému účelu, jako například zcela odlišným způsobem koncipovaná makra použitá v programovacích jazycích C a C++. V předchozím článku jsme si taktéž ukázali dva způsoby tvorby uživatelských maker. První způsob spočíval v poněkud namáhavé konstrukci výsledného kódu s využitím funkcí a speciálních forem list, quote a concat. Vážnou nevýhodou tohoto způsobu tvorby maker je především dlouhý i nepřehledný zápis makra a taktéž to, že se uživatel při tvorbě složitějšího makra obvykle nevyhne nutnosti toto makro několikrát přepsat, protože někde zapomene uvést quote, nebo naopak quote přidá i tam, kde ve skutečnosti nemá být.

Připomeňme si nejdříve, jak jsme vytvořili první uživatelské makro. Toto makro se jmenovalo trace a když bylo zavoláno s určitým výrazem, mělo dojít k vygenerování kódu, který nejdříve vypsal tento výraz (přesněji řečeno jeho tisknutelnou podobu) a následně vypsal i hodnotu vzniklou vyhodnocením tohoto výrazu. Současně měla být tato hodnota i návratovou hodnotou vygenerovaného kódu, aby se vložením makra do složitějšího programu neovlivnila správná funkce tohoto programu. Kód, který měl být makrem trace vygenerován, vypadá zhruba takto:

(let [x vyraz]
    (println (quote vyraz) " = " x)
    x)

Můžeme zde vidět použití lokální proměnné x, díky níž se předaný výraz vyhodnotí pouze jednou, což je nutné zajistit zejména tehdy, pokud by nějaká funkce volaná z výrazu měla vedlejší efekt. Abychom vytvořili tento výraz ve formě rekurzivně zanořeného seznamu, definovali jsme makro trace s následující podobou:

(defmacro trace
    [vyraz]
    (list
        'let ['x vyraz]
        (list 'println (list 'quote vyraz) "=" 'x)
        'x))

O tom, že makro pracuje (alespoň zdánlivě) korektně, jsme se mohli přesvědčit použitím funkce macroexpand-1, která zobrazí expandované makro:

; nejprve zadáme interpretru tvar makra
(defmacro trace
    [vyraz]
    (list
        'let ['x vyraz]
        (list 'println (list 'quote vyraz) "=" 'x)
        'x))
 
 
; otestování makra na volání jednoduché funkce
user=> (trace (* 6 7))
(* 6 7) = 42
42
 
 
; zobrazíme si expandované makro
user=> (macroexpand-1 '(trace (* 6 7)))
(let [x (* 6 7)] (println (quote (* 6 7)) "=" x) x)
 
 
user=>

2. Zápis kódu generovaného makrem s využitím „šablony“

Zápis makra uvedený v předchozí kapitole ve skutečnosti není ani příliš pěkný, ani čitelný, protože jsme byli nuceni všechny funkce (které se měly objevit ve výsledném kódu) quotovat, tj. zapsat před ně znak apostrofu, což je zkrácený způsob zápisu (quote …). Dále se výsledný seznam musel explicitně vytvářet s využitím funkce list atd. Mnohem čitelnější a elegantnější je použít zápis makra využívající takzvaná reader makra nazvaná „syntax-quote“ (zapisováno formou znaku zpětného apostrofu, back-tick) a „unquote“ (zapisováno jako znak tilda). S využitím zpětného apostrofu bylo možné tělo makra, tj. generovaný výraz, quotovat, ovšem s tou výjimkou, že symboly, před nimiž byl zapsán znak tildy („unquote“) byly naopak nahrazeny hodnotou přiřazenou k tomuto symbolu. S využitím znaků ` a ~ je tedy možné zapsat makro ve formě jakési šablony (template), do které se pouze doplňují hodnoty vybraných symbolů. Druhá verze makra trace je nepochybně jednodušší a čitelnější než verze první. Povšimněte si především toho, že v makru můžeme použít například i '~vyraz, protože znak ' („quote“) je uvnitř ` pouze opisován do kódu generovaného makrem a nemá zde tedy svůj obvyklý význam:

(defmacro trace2
    [vyraz]
    `(let [x ~vyraz] (println '~vyraz "=" x) x))

Použití znaku ` však přineslo ještě jeden efekt – ke všem symbolům použitým v makru se přidal i jejich jmenný prostor (namespace/ns), což je sice zcela správně (makro bude možné použít i v jiném jmenném prostoru), ovšem jmenný prostor se doplnil i u symbolu x, který by měl být ve skutečnosti symbolem lokálním. Aby makro fungovalo zcela správně, musíme umět vytvořit lokální symbol s unikátním jménem. Pro tento účel se používá (tak jako v mnoha dalších situacích) znak křížku zapisovaný ZA nějakým symbolem. Clojure zápis symbol# expanduje na symbol_generované_číslo, které bude unikátní, což je přesně to, co potřebujeme. Ve třetí verzi našeho makra tedy nahradíme x za x#:

(defmacro trace3
    [vyraz]
    `(let [x# ~vyraz] (println '~vyraz "=" x#) x#))

Nyní tedy již známe téměř všechny nástroje používané při tvorbě maker:

Znak Význam při tvorbě maker
` většinou uzavírá celé tělo makra, tj. šablonu vytvářeného výrazu
~ ruší význam znaku `, ovšem pouze pro jediný symbol uvedený ihned za tímto znakem
# symbol zapsaný před tímto znakem bude doplněn takovým způsobem, aby se jednalo o unikátní jméno

Ještě jsme si nevysvětlili význam dvouznaku ~@ (reader makro „unqote-splicing“), který bude využit v následujících kapitolách.

3. Příprava na vylepšení makra trace

Prozatím poslední varianta makra trace, která byla ukázána v předchozí kapitole, sice pracuje korektně, ale možná jste si všimli, že při použití tohoto makra je nutné při jeho vkládání do zdrojových kódů nejenom uvést název tohoto makra, ale navíc se musí okolo předávané funkce/výrazu/formy napsat i kulaté závorky:

; otestování makra na volání funkce
user=> (trace (* 6 7))
(* 6 7) = 42
42
 
 
; volání funkce apply
user=> (trace4 (apply * (range 1 6)))
(apply * (range 1 6)) = 120
120
 
 
; otestování makra na volání složitější funkce
user=> (trace (list (range 1 10) (range 20 30)))
(list (range 1 10) (range 20 30)) = ((1 2 3 4 5 6 7 8 9) (20 21 22 23 24 25 26 27 28 29))
((1 2 3 4 5 6 7 8 9) (20 21 22 23 24 25 26 27 28 29))
 
 
user=>

Toto chování samozřejmě není zcela špatné (použití makra se ze syntaktického hlediska neliší od volání běžné funkce či speciální formy), ovšem použití makra trace se stává poněkud složitější, než by mohlo být, protože ve chvíli, kdy potřebujeme do zdrojového kódu toto makro vložit (a vytvářet tak trasovací informace při běhu aplikace), je nutné nejenom před nějaký výraz zapsat (trace, ale dále je nutné najít ještě odpovídající pravou kulatou závorku a celé volání uzavřít pomocí ). To je již komplikovaný zásah do zdrojových kódů, zejména v případě, že programátor rád tvoří složité výrazy s mnoha závorkami, kvůli nimž se význam názvu programovacího jazyka LISP (z něhož je Clojure přímo odvozen) vysvětluje jako Lost In Stupid Parentheses. Jedním z důvodů existence maker je zjednodušit zápis programů, ne ho ještě udělat více složitý. Naším cílem tedy bude upravit makro trace takovým způsobem, abychom mohli psát prostě:

; otestování makra na volání funkce
user=> (trace * 6 7)
(* 6 7) = 42
42
 
 
; volání funkce range
user=> (trace range 1 10)
(range 1 10) = (1 2 3 4 5 6 7 8 9)
(1 2 3 4 5 6 7 8 9)
 
 
; volání funkce apply
user=> (trace4 apply * (range 1 6))
(apply * (range 1 6)) = 120
120
 
 
; otestování makra na volání složitější funkce
user=> (trace list (range 1 10) (range 20 30))
(list (range 1 10) (range 20 30)) = ((1 2 3 4 5 6 7 8 9) (20 21 22 23 24 25 26 27 28 29))
((1 2 3 4 5 6 7 8 9) (20 21 22 23 24 25 26 27 28 29))
 
 
user=>

Asi nebudete příliš překvapeni zjištěním, že makro půjde přesně takovým způsobem upravit, a to relativně jednoduchým způsobem. Ještě před popisem potřebné úpravy si však musíme vysvětlit další vlastnost programovacího jazyka Clojure: způsob tvorby funkcí s proměnným počtem parametrů.

4. Funkce s proměnným počtem parametrů

V mnoha programovacích jazycích se jejich tvůrci snažili více či méně elegantně vyřešit problém tvorby funkcí či metod s proměnným počtem parametrů. Nejedná se sice o vlastnost, která by musela být v programovacím jazyku nutně implementována, ovšem její zahrnutí do syntaxe a sémantiky jazyka znamená pro programátory možnost tvorby univerzálněji použitelných funkcí. Zejména se tak dá efektivně obejít vytváření přetížených funkcí či metod, které jsou mnohdy přetěžovány jen z toho důvodu, že v některých případech jim potřebujeme předat doplňující parametry. Zajímavé je, že dokonce i v těch programovacích jazycích, které (třeba jen zpočátku) možnost tvorby funkcí s proměnným počtem parametrů nepovolovaly, se tato vlastnost objevila, například ve formě „speciálních příkazů“, které sice programátoři nemohli sami vytvářet, ale mohli alespoň tyto již připravené speciální příkazy používat (což na druhou stranu ukazuje na nekoncepčnost při návrhu těchto jazyků).

Pro příklady nemusíme chodit daleko – již programovací jazyk Pascal obsahoval „speciální příkazy“ write a writeln s proměnným počtem parametrů, stejně jako programovací jazyk Basic s jeho příkazy PRINT a INPUT.

Tvůrci některých dalších programovacích jazyků byli vůči programátorům-uživatelům férovější a zahrnuli tuto možnost přímo do syntaxe a sémantiky. Týká se to například programovacích jazyků C, C++, TCL, Python a podpory pro proměnný počet parametrů metod jsme se nakonec dočkali i v Javě (i když zde je možná typické, že nejprve musel přijít konkurent jménem C#, aby se tvůrci Javy konečně „rozhoupali“ pro implementaci nové vlastnosti do jazyka). Podporu pro proměnný počet parametrů obsahuje i programovací jazyk Clojure, v němž je možné s využitím znaku & oddělit lokální jména povinných parametrů od jména sekvence, do níž se uloží všechny zbylé parametry. Povinné parametry se označují termínem „poziční parametry“, zbylé parametry pak termínem „další (next) parametry“. Podívejme se nejprve na způsob zápisu funkce obsahující pouze dva poziční parametry (nejedná se o nic nového – podobný zápis byl v tomto seriálu použit již mockrát):

(defn fce1
    [x y]
    (println "x = " x)
    (println "y = " y))

Použití této funkce je snadné:

user=> (fce1 1 2)
x =  1
y =  2
nil
 
 
user=> (fce1 "Hello" "world")
x =  Hello
y =  world
nil
 
 
user=>

Musíme ovšem přesně dodržet počet předávaných parametrů, jinak dojde při zavolání funkce k chybě:

user=> (fce1 1)
ArityException Wrong number of args (1) passed to: user$fce1  clojure.lang.AFn.throwArity (AFn.java:437)
 
 
user=> (fce1 1 2 3)
ArityException Wrong number of args (3) passed to: user$fce1  clojure.lang.AFn.throwArity (AFn.java:437)
user=>
 
 
user=>

5. Příklad funkce s proměnným počtem parametrů

Pokusme se funkci fce1 z předchozí kapitoly upravit takovým způsobem, aby akceptovala jeden povinný (poziční) parametr a poté libovolné množství dalších parametrů. Úprava je v tomto případě velmi snadná, protože postačuje vložit znak ampersandu (&) mezi jména obou formálních parametrů. Počet jmen formálních parametrů před ampersandem totiž odpovídá počtu povinných parametrů, s nimiž se funkce může volat, za ampersandem je pak jméno symbolu s lokální platností, do něhož se ukládají zbylé parametry. Jelikož jejich počet není dopředu znám, je k tomuto symbolu buď přiřazena hodnota nil (žádný volitelný parametr nebyl předán), nebo je mu přiřazen seznam (byl předán alespoň jeden a nebo více volitelných parametrů):

(defn fce2
    [x & y]
    (println "x = " x)
    (println "y = " y))

Použití této funkce je již mnohem zajímavější, protože se všechny nepovinné parametry vkládají do seznamu navázaného na lokální symbol y:

; dva parametry - první je uložen do x, druhý
; do prvního prvku seznamu y
user=> (fce2 1 2)
x =  1
y =  (2)
nil
 
 
; dva parametry - první je uložen do x, druhý
; do prvního prvku seznamu y
user=> (fce2 "Hello" "world")
x =  Hello
y =  (world)
nil
 
 
; tři parametry - první je uložen do x, druhý
; a třetí do seznamu y
user=> (fce2 "Don't" "panic" '!)
x =  Don't
y =  (panic !)
nil
 
 
; pouze jeden parametr uložený do x
; lokální symbol y je přiřazen k hodnotě nil!!!
user=> (fce2 1)
x =  1
y =  nil
nil
 
 
; tři parametry - první je uložen do x, druhý
; a třetí do seznamu y
user=> (fce2 1 2 3)
x =  1
y =  (2 3)
nil
 
 
; složitější příklad, kdy se do y vloží seznam
; obsahující jako své prvky další podseznamy
user=> (fce2 1 '(2 3) '(4 5) '(6 7))
x =  1
y =  ((2 3) (4 5) (6 7))
nil
 
 
user=>

Ve skutečnosti ovšem můžeme zajít ještě dále a vytvořit funkci akceptující skutečně úplně libovolný počet parametrů, včetně žádného parametru:

(defn fce3
    [& x]
    (println "x = " x))

Opět si uveďme několik demonstračních příkladů:

; dva parametry - budou uloženy do seznamu
; na nějž bude navázán symbol x
user=> (fce3 1 2)
x =  (1 2)
nil
 
 
; dva parametry - budou uloženy do seznamu
; na nějž bude navázán symbol x
user=> (fce3 "Hello" "world")
x =  (Hello world)
nil
 
 
; tři parametry - budou uloženy do seznamu
; na nějž bude navázán symbol x
user=> (fce3 "Don't" "panic" '!)
x =  (Don't panic !)
nil
 
 
; pouze jediný parametr - seznam se však přesto vytvoří
user=> (fce3 1)
x =  (1)
nil
 
 
; pokud funkci nepředáme žádný parametr,
; bude x navázán na hodnotu nil!!!
user=> (fce3)
x =  nil
nil
 
 
user=>

6. Vylepšené makro trace

Změna makra trace takovým způsobem, aby toto makro akceptovalo libovolný počet parametrů, přičemž první parametr bude jméno funkce, je poměrně snadné. Zatímco předchozí tvar makra byl tento:

(defmacro trace3
    [vyraz]
    `(let [x# ~vyraz] (println '~vyraz "=" x#) x#))

…nyní upravíme makro tak, aby se všechny předávané parametry uložily do seznamu. Při vyhodnocení předaného „výrazu“ (nejedná se o skutečný výraz, přesněji řečeno nejde o korektní formu) přitom využijeme známého faktu, že seznam může být považován za volání funkce – jméno funkce je uloženo v prvním prvku seznamu, zbylé prvky pak představují parametry předané této funkci. Znak & má stejný význam při definici funkcí pomocí defn i při definici maker s využitím defmacro, tudíž je následující zápis korektní:

(defmacro trace4
    [& params]
    `(let [x# ~params] (println '~params "=" x#) x#))

Vyzkoušejme si „vylepšenou“ funkci tohoto makra. Povšimněte si, že pokud je zapotřebí někam do kódu přidat generování trasovacích informací, postačuje na toto místo kódu vložit pouze symbol trace4. To znamená, že žádné závorky ani další znaky se již (až na jednu výjimku) nemusí používat:

; zde může dojít k překvapení
user=> (trace4)
nil = nil
nil
 
 
user=> (trace4 + 1 2)
(+ 1 2) = 3
3
 
 
user=> (trace4 Math/random)
(Math/random) = 0.5518909120897316
0.5518909120897316
 
 
user=> (trace4 apply * (range 1 6))
(apply * (range 1 6)) = 120
120
 
 
user=>

Zajímavá taktéž může být expanze tohoto makra, zejména v prvním případě, protože zde se namísto seznamu přiřadí symbolu params hodnota nil:

user=> (macroexpand-1 '(trace4))
(clojure.core/let [x__1__auto__ nil] (clojure.core/println (quote nil) "=" x__1_
_auto__) x__1__auto__)
 
 
user=> (macroexpand-1 '(trace4 Math/random))
(clojure.core/let [x__1__auto__ (Math/random)] (clojure.core/println (quote (Mat
h/random)) "=" x__1__auto__) x__1__auto__)
 
 
user=> (macroexpand-1 '(trace4 apply * (range 1 6)))
(clojure.core/let [x__1__auto__ (apply * (range 1 6))] (clojure.core/println (qu
ote (apply * (range 1 6))) "=" x__1__auto__) x__1__auto__)
 
 
user=>

Malý úkol pro čtenáře: makro trace4 ve skutečnosti nebude správně fungovat ve chvíli, kdy se mu předá například atom – celé číslo, pravdivostní hodnota atd. Zkuste analyzovat následující dvojici příkazů (volání makra a jeho expanzi) a navrhnout takovou úpravu makra, která tuto chybu eliminuje:

user=> (trace4 42)
ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn  user/eval1
35 (NO_SOURCE_FILE:72)
 
 
user=> (macroexpand-1 '(trace4 42))
(clojure.core/let [x__1__auto__ (42)] (clojure.core/println (quote (42)) "=" x__
1__auto__) x__1__auto__)
 
 
user=>

7. Vytvoření nového typu programové smyčky forloop s využitím maker

Nyní se pustíme do tvorby složitějšího makra, konkrétně do makra představujícího programovou smyčku, která tak vlastně rozšíří syntaxi a sémantiku programovacího jazyka Clojure o novou konstrukci. Postup při tvorbě tohoto makra je velmi detailně popsán v trojici článků „Clojure Macro Tutorial“ – odkaz na tyto články najdete na začátku deváté kapitoly (při prvním čtení je možná nejlepší články prostudovat v pořadí I.díl, III.díl a teprve nakonec II.díl). Nicméně se vraťme k naší programové smyčce. Potřebovali bychom vytvořit počítanou smyčku typu for, které by se předala trojice parametrů – název řídicí proměnné smyčky (počitadla), počáteční hodnota počitadla a koncová hodnota počitadla. Za parametry by mělo následovat tělo smyčky, v němž je možné využít řídicí proměnnou, popř. globální proměnné atd. Pro jednoduchost předpokládejme, že krok smyčky bude vždy roven jedné, i když ve skutečnosti nebude větším problémem naše makro rozšířit i o možnost zadání libovolného jiného kroku. Způsob volání tohoto makra by měl vypadat zhruba následovně:

(forloop [counter start end]
    code)

Makro by mělo na základě zadaných údajů vygenerovat tento kód, v němž je použita tail rekurze:

(loop [counter start]
    (when (<= counter end)
        code
        (recur (inc counter))))

Konkrétní příklad:

(forloop [i 1 100]
    (println i))

Kód vygenerovaný makrem forloop:

(loop [i 1]
    (when (<= i 100)
        (println i)
        (recur (inc i))))

Při tvorbě a ladění maker můžeme postupovat dvěma způsoby. První způsob spočívá v tom, že se nevytváří přímo makro pomocí defmacro, ale namísto toho se vytváří funkce sloužící jako generátor kódu. Vygenerovaný kód se buď pouze zobrazí, nebo je ho možné kdykoli vyhodnotit pomocí eval. Druhý způsob spočívá v přímém vytvoření makra a následně v použití funkce macroexpand-1 pro prohlédnutí expandovaného makra. Podle mě je první způsob výhodnější, proto ho také použijeme zde. Následující funkci, která je pojmenovaná forloop-generator, se musí předat vektor s trojicí hodnot [jméno-řídicí-proměnné počáteční-hodnota-proměnné koncová-hodnota-proměnné] a za tímto vektorem může následovat kód, který odpovídá tělu generované smyčky. Žádné nové techniky jsme v této funkci nepoužili, dokonce ani prozatím nepoužíváme reader makra „syntax-quote“ či „unquote“:

(defn forloop-generator
    [[variable start end] & code]
    (list 'loop [variable start]
        (concat (list 'when)
            (list (list '<= variable end))
            code
            (list (list 'recur (list 'inc variable))))))

Nyní se můžeme podívat na kód generovaný touto funkcí, nesmíme však zapomenout na to, že jak jméno řídicí proměnné, tak i tělo smyčky je zapotřebí quotovat, protože (prozatím) nevoláme plnohodnotné makro, ale obyčejnou funkci:

user=> (forloop-generator '[i 1 10] '(println i))
(loop [i 1] (when (<= i 10) (println i) (recur (inc i))))
 
 
user=> (forloop-generator ['i 1 10] '(println i))
(loop [i 1] (when (<= i 10) (println i) (recur (inc i))))
 
 
user=>

S využitím eval nakonec můžeme vyzkoušet, zda lze generovaný kód vůbec spustit (vyhodnotit) a zda dostaneme očekávaný výsledek:

user=> (eval (forloop-generator ['i 1 10] '(println i)))
1
2
3
4
5
6
7
8
9
10
nil
 
 
user=>

8. Úprava programové smyčky forloop

Pokusme se nyní nahradit nečitelné generování kódu s využitím quote, list a concat za generování s použitím šablony, tj. za pomoci reader maker „syntax-quote“ a „unquote“. Postup je velmi jednoduchý a už jsme si ho ukazovali v předchozích kapitolách – celý generovaný kód bude uvozen znakem zpětného apostrofu („syntax-quote“) a pouze tam, kde budeme potřebovat použít hodnotu nějakého symbolu použijeme znak tildy před jménem tohoto symbolu („unquote“). Druhá verze našeho generátoru kódu tedy bude vypadat následovně:

(defn forloop-generator2
    [[variable start end] & code]
    `(loop [~variable ~start]
        (when (<= ~variable ~end)
            ~code
            (recur (inc ~variable)))))

Na první pohled prozatím není v tomto zápisu patrná žádná chyba, proto si generátor kódu vyzkoušíme:

user=> (forloop-generator2 '[i 1 10] '(println i))
(clojure.core/loop [i 1] (clojure.core/when (clojure.core/<= i 10) ((println i))
 (recur (clojure.core/inc i))))
 
 
user=>

Vidíme, že jména symbolů byla rozšířena o jmenný prostor, což je dobře. Dále se v kódu správně nahradily části ~variable, ~start a ~end za i, 1 a 10, což je taktéž správně. Jediný problém však spočívá v tom, že tělo smyčky má okolo sebe ještě jedny kulaté závorky, což již korektní není. O tom, že se skutečně jedná o chybu, se můžeme snadno přesvědčit ve chvíli, kdy se pokusíme smyčku zavolat, tj. vyhodnotit:

user=< (eval (forloop-generator2 '[i 1 10] '(println i)))
1
NullPointerException   user/eval36 (NO_SOURCE_FILE:26)
 
 
user=>

Jak lze této chybě zabránit? Quotování použité při předávání těla smyčky do generátoru kódu nemůžeme odstranit, protože by se interpret snažil vyhodnotit (println i) ještě před spuštěním funkce forloop-generator2. Druhé závorky se vytvořily proto, že symboly tvořící tělo smyčky jsou předávány formou nepovinných parametrů, které se vždy ukládají do seznamu. Aby se tato chyba napravila, musíme být schopni nějak zploštit seznam představující tělo smyčky, tj. získat z tohoto seznamu pouze jeho prvky. To je operace, kterou provádí poslední reader makro nazvané „unquote-splicing“, které se zapisuje dvojicí znaků ~@. Třetí verze generátoru smyčky bude vypadat následovně:

(defn forloop-generator3
    [[variable start end] & code]
    `(loop [~variable ~start]
        (when (<= ~variable ~end)
            ~@code
            (recur (inc ~variable)))))

Nezbývá než tuto verzi smyčky otestovat:

user=> (forloop-generator3 '[i 1 10] '(println i))
(clojure.core/loop [i 1] (clojure.core/when (clojure.core/<= i 10) (println i) (
recur (clojure.core/inc i))))
 
 
user=> (eval (forloop-generator3 '[i 1 10] '(println i)))
1
2
3
4
5
6
7
8
9
10
nil
 
 
user=>

Chování se zdá být v pořádku, takže můžeme přistoupit k vytvoření makra, což je jednoduché, protože prostě nahradíme defn za defmacro:

ict ve školství 24

(defmacro forloop
    [[variable start end] & code]
    `(loop [~variable ~start]
        (when (<= ~variable ~end)
            ~@code
            (recur (inc ~variable)))))

A ihned můžeme toto makro odzkoušet – tentokrát již samozřejmě není zapotřebí (a ani to není korektní) quotovat předávaný vektor ani tělo smyčky!

user=> (forloop [x 1 10] (println x))
1
2
3
4
5
6
7
8
9
10
nil
 
 
user=> (forloop [x 1 10] (println (* x x)))
1
4
9
16
25
36
49
64
81
100
nil
 
 
user=>
 
; finito

Zbývá nám pouze dořešit jediný zbývající problém, což je druhý úkol pro čtenáře. Makro je vhodné upravit takovým způsobem, aby se koncová hodnota počitadla vyhodnocovala pouze jedenkrát a nikoli na začátku každé iterace. V našich demonstračních příkladech jsme jako koncovou hodnotu používali celé číslo, ovšem kdyby se jednalo o volání funkce s vedlejším efektem, vznikl by problém. Řešení této chyby není ve skutečnosti těžké – postačuje použít lokální generovaný symbol naplněný ve speciální formě let, která bude celou smyčku „obalovat“ – viz též poslední verzi makra trace, kde se používá podobné řešení.

9. Odkazy na Internetu

  1. Clojure Macro Tutorial (Part I, Getting the Compiler to Write Your Code For You)
    http://www.learningclojure­.com/2010/09/clojure-macro-tutorial-part-i-getting.html
  2. Clojure Macro Tutorial (Part II: The Compiler Strikes Back)
    http://www.learningclojure­.com/2010/09/clojure-macro-tutorial-part-ii-compiler.html
  3. Clojure Macro Tutorial (Part III: Syntax Quote)
    http://www.learningclojure­.com/2010/09/clojure-macro-tutorial-part-ii-syntax.html
  4. Tech behind Tech: Clojure Macros Simplified
    http://techbehindtech.com/2010/09/28/clo­jure-macros-simplified/
  5. Fatvat – Exploring functional programming: Clojure Macros
    http://www.fatvat.co.uk/2009/02/clo­jure-macros.html
  6. Eulerovo číslo
    http://cs.wikipedia.org/wi­ki/Eulerovo_číslo
  7. List comprehension
    http://en.wikipedia.org/wi­ki/List_comprehension
  8. List Comprehensions in Clojure
    http://asymmetrical-view.com/2008/11/18/list-comprehensions-in-clojure.html
  9. Clojure Programming Concepts: List Comprehension
    http://en.wikibooks.org/wi­ki/Clojure_Programming/Con­cepts#List_Comprehension
  10. Clojure core API: for macro
    http://clojure.github.com/clo­jure/clojure.core-api.html#clojure.core/for
  11. cirrus machina – The Clojure for macro
    http://www.cirrusmachina.com/blog/com­ment/the-clojure-for-macro/
  12. Clojure.org: Clojure home page
    http://clojure.org/downloads
  13. Clojure.org: Vars and the Global Environment
    http://clojure.org/Vars
  14. Clojure.org: Refs and Transactions
    http://clojure.org/Refs
  15. Clojure.org: Atoms
    http://clojure.org/Atoms
  16. Clojure.org: Agents as Asynchronous Actions
    http://clojure.org/agents
  17. A Couple of Clojure Agent Examples
    http://lethain.com/a-couple-of-clojure-agent-examples/
  18. Clojure – Functional Programming for the JVM
    http://java.ociweb.com/mar­k/clojure/article.html
  19. Clojure quick reference
    http://faustus.webatu.com/clj-quick-ref.html
  20. 4Clojure
    http://www.4clojure.com/
  21. ClojureDoc
    http://clojuredocs.org/
  22. Clojure (Wikipedia EN)
    http://en.wikipedia.org/wiki/Clojure
  23. Clojure (Wikipedia CS)
    http://cs.wikipedia.org/wiki/Clojure
  24. Riastradh's Lisp Style Rules
    http://mumble.net/~campbe­ll/scheme/style.txt
  25. Dynamic Languages Strike Back
    http://steve-yegge.blogspot.cz/2008/05/dynamic-languages-strike-back.html
  26. Scripting: Higher Level Programming for the 21st Century
    http://www.tcl.tk/doc/scripting.html
  27. Java Virtual Machine Support for Non-Java Languages
    http://docs.oracle.com/ja­vase/7/docs/technotes/gui­des/vm/multiple-language-support.html
  28. New JDK 7 Feature: Support for Dynamically Typed Languages in the Java Virtual Machine
    http://java.sun.com/develo­per/technicalArticles/Dyn­TypeLang/
  29. JSR 223: Scripting for the JavaTM Platform
    http://jcp.org/en/jsr/detail?id=223
  30. JSR 292: Supporting Dynamically Typed Languages on the JavaTM Platform
    http://jcp.org/en/jsr/detail?id=292
  31. Java 7: A complete invokedynamic example
    http://niklasschlimm.blog­spot.com/2012/02/java-7-complete-invokedynamic-example.html
  32. InvokeDynamic: Actually Useful?
    http://blog.headius.com/2007/01/in­vokedynamic-actually-useful.html
  33. A First Taste of InvokeDynamic
    http://blog.headius.com/2008/09/first-taste-of-invokedynamic.html
  34. Java 6 try/finally compilation without jsr/ret
    http://cliffhacks.blogspot­.com/2008/02/java-6-tryfinally-compilation-without.html
  35. An empirical study of Java bytecode programs
    http://www.mendeley.com/research/an-empirical-study-of-java-bytecode-programs/
  36. Java quick guide: JVM Instruction Set (tabulka všech instrukcí JVM)
    http://www.mobilefish.com/tu­torials/java/java_quickgu­ide_jvm_instruction_set.html
  37. The JVM Instruction Set
    http://mpdeboer.home.xs4a­ll.nl/scriptie/node14.html
  38. Control Flow in the Java Virtual Machine
    http://www.artima.com/under­thehood/flowP.html
  39. Root.cz: Využití komprimovaných ukazatelů na objekty v JVM
    http://www.root.cz/clanky/vyuziti-komprimovanych-ukazatelu-na-objekty-v-nbsp-jvm/
  40. Root.cz: JamVM aneb alternativa k HotSpotu nejenom pro embedded zařízení a chytré telefony
    http://www.root.cz/clanky/jamvm-aneb-alternativa-k-hotspotu-nejenom-pro-embedded-zarizeni-tablety-a-chytre-telefony/
  41. The JavaTM Virtual Machine Specification, Second Edition
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/VMSpec­TOC.doc.html
  42. The class File Format
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/Clas­sFile.doc.html
  43. javap – The Java Class File Disassembler
    http://docs.oracle.com/ja­vase/1.4.2/docs/tooldocs/win­dows/javap.html
  44. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.die.net/man/1/javap-java-1.6.0-openjdk
  45. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  46. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  47. BCEL Home page
    http://commons.apache.org/bcel/
  48. BCEL Manual
    http://commons.apache.org/bcel/ma­nual.html
  49. Byte Code Engineering Library (Wikipedia)
    http://en.wikipedia.org/wiki/BCEL
  50. Java programming dynamics, Part 7: Bytecode engineering with BCEL
    http://www.ibm.com/develo­perworks/java/library/j-dyn0414/
  51. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.html
  52. BCEL Tutorial
    http://www.smfsupport.com/sup­port/java/bcel-tutorial!/
  53. ASM Home page
    http://asm.ow2.org/
  54. Seznam nástrojů využívajících projekt ASM
    http://asm.ow2.org/users.html
  55. ObjectWeb ASM (Wikipedia)
    http://en.wikipedia.org/wi­ki/ObjectWeb_ASM
  56. Java Bytecode BCEL vs ASM
    http://james.onegoodcooki­e.com/2005/10/26/java-bytecode-bcel-vs-asm/
  57. Bytecode Outline plugin for Eclipse (screenshoty + info)
    http://asm.ow2.org/eclipse/index.html
  58. aspectj (Eclipse)
    http://www.eclipse.org/aspectj/
  59. Aspect-oriented programming (Wikipedia)
    http://en.wikipedia.org/wi­ki/Aspect_oriented_program­ming
  60. AspectJ (Wikipedia)
    http://en.wikipedia.org/wiki/AspectJ
  61. EMMA: a free Java code coverage tool
    http://emma.sourceforge.net/
  62. Cobertura
    http://cobertura.sourceforge.net/
  63. FindBugs
    http://findbugs.sourceforge.net/
  64. GNU Classpath
    www.gnu.org/s/classpath/
  65. Java VMs Compared
    http://bugblogger.com/java-vms-compared-160/
  66. JSRs: Java Specification Requests – JSR 223: Scripting for the Java Platform
    http://www.jcp.org/en/jsr/de­tail?id=223
  67. Scripting for the Java Platform
    http://java.sun.com/develo­per/technicalArticles/J2SE/Des­ktop/scripting/
  68. Scripting for the Java Platform (Wikipedia)
    http://en.wikipedia.org/wi­ki/Scripting_for_the_Java_Plat­form
  69. Java Community Process
    http://en.wikipedia.org/wi­ki/Java_Specification_Requ­est
  70. Java HotSpot VM Options
    http://www.oracle.com/technet­work/java/javase/tech/vmop­tions-jsp-140102.html
  71. Great Computer Language Shootout
    http://c2.com/cgi/wiki?Gre­atComputerLanguageShootout
  72. Java performance
    http://en.wikipedia.org/wi­ki/Java_performance
  73. Trying the prototype
    http://mail.openjdk.java.net/pi­permail/lambda-dev/2010-August/002179.html
  74. Better closures (for Java)
    http://blogs.sun.com/jrose/en­try/better_closures
  75. Lambdas in Java: An In-Depth Analysis
    http://www.infoq.com/articles/lambdas-java-analysis
  76. Class ReflectiveOperationException
    http://download.java.net/jdk7/doc­s/api/java/lang/Reflective­OperationException.html
  77. Scala Programming Language
    http://www.scala-lang.org/
  78. Run Scala in Apache Tomcat in 10 minutes
    http://www.softwaresecret­weapons.com/jspwiki/run-scala-in-apache-tomcat-in-10-minutes
  79. Fast Web Development With Scala
    http://chasethedevil.blog­spot.cz/2007/09/fast-web-development-with-scala.html
  80. Top five scripting languages on the JVM
    http://www.infoworld.com/d/developer-world/top-five-scripting-languages-the-jvm-855
  81. Proposal: Indexing access syntax for Lists and Maps
    http://mail.openjdk.java.net/pi­permail/coin-dev/2009-March/001108.html
  82. Proposal: Elvis and Other Null-Safe Operators
    http://mail.openjdk.java.net/pi­permail/coin-dev/2009-March/000047.html
  83. Java 7 : Oracle pushes a first version of closures
    http://www.baptiste-wicht.com/2010/05/oracle-pushes-a-first-version-of-closures/
  84. Groovy: An agile dynamic language for the Java Platform
    http://groovy.codehaus.org/Operators
  85. Better Strategies for Null Handling in Java
    http://www.slideshare.net/Step­han.Schmidt/better-strategies-for-null-handling-in-java
  86. Control Flow in the Java Virtual Machine
    http://www.artima.com/under­thehood/flowP.html
  87. Java Virtual Machine
    http://en.wikipedia.org/wi­ki/Java_virtual_machine
  88. ==, .equals(), compareTo(), and compare()
    http://leepoint.net/notes-java/data/expressions/22com­pareobjects.html
  89. New JDK7 features
    http://openjdk.java.net/pro­jects/jdk7/features/
  90. Project Coin: Bringing it to a Close(able)
    http://blogs.sun.com/darcy/en­try/project_coin_bring_clo­se
  91. CloseableFinder source code
    http://blogs.sun.com/darcy/re­source/ProjectCoin/Closea­bleFinder.java
  92. Joe Darcy blog about JDK
    http://blogs.sun.com/darcy
  93. Java 7 – more dynamics
    http://www.baptiste-wicht.com/2010/04/java-7-more-dynamics/
  94. New JDK 7 Feature: Support for Dynamically Typed Languages in the Java Virtual Machine
    http://java.sun.com/develo­per/technicalArticles/Dyn­TypeLang/index.html

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.