Clojure aneb jazyk umožňující tvorbu bezpečných vícevláknových aplikací pro JVM (3.část - funkcionální programování)

26. 6. 2012
Doba čtení: 22 minut

Sdílet

V dnešní části seriálu o programovacím jazyce Java i o vlastnostech JVM se již potřetí vrátíme k popisu programovacího jazyka Clojure. Zatímco v předchozích dvou částech jsme se zabývali především popisem způsobu zápisu různých forem, dnes se již konečně začneme zabývat tvorbou programů.

Obsah

1. Tvorba skutečných aplikací programů v programovacím jazyce Clojure

2. Speciální forma def

3. Jmenné prostory

4. Role a vlastnosti funkcí ve funkcionálních programovacích jazycích

5. Vytvoření uživatelských funkcí

6. Zjednodušená deklarace funkcí, funkce s více aritami

7. Programové smyčky ve funkcionálních jazycích? Nikoli, spíše rekurze!

8. Možné problémy s rekurzí a řešení těchto problémů – tail rekurze

9. Odkazy na Internetu

1. Tvorba skutečných aplikací v programovacím jazyce Clojure

V předchozí části seriálu o programovacím jazyce Java i o vlastnostech virtuálního stroje tohoto jazyka jsme dokončili popis způsobu zápisu všech čtyř typů forem, z nichž se skládají všechny zdrojové kódy napsané v programovacím jazyce Clojure. Taktéž jsme si řekli, že s využitím takzvaných složených forem je možné zapsat jak kolekce (v Clojure především seznamy, vektory, množiny, mapy – asociativní pole), tak i volání funkcí, protože způsob zápisu volání funkcí odpovídá v Clojure – a taktéž v LISPu či Scheme – zápisu seznamu, přičemž první prvek seznamu v tomto případě představuje jméno funkce (resp. přesněji řečeno symbol navázaný na funkci) a další prvky seznamu jsou (po vyhodnocení) volané funkci předány jako její parametry. Připomeňme si také, že kromě složených forem bylo nutné programovací jazyk Clojure vybavit i takzvanými speciálními formami, které jsou obecně zpracovávány odlišným způsobem – většinou se například nevyhodnocují jejich parametry.

Jen pro malé připomenutí si v této kapitole uvedeme několik demonstračních příkladů, na nichž bude ukázán způsob použití složených forem (seznamů, vektorů, množin a map) a taktéž speciálních forem, přesněji řečeno prozatím jen speciální formy if.

Základní složenou formou je seznam (list), což ostatně nebude velkým překvapením, neboť Clojure je potomek jazyka LISP, jehož název je odvozen od sousloví „LISt Processing“. V následujícím příkladu je vytvořen seznam obsahující další seznamy (zde se vlastně jedná o nevyvážený binární strom). Apostrof před seznamem musí být uveden z toho důvodu, aby se celý zápis nechápal jako volání funkce navázané na symbol 1. Taková funkce totiž neexistuje a ani existovat nemůže, neboť jméno symbolu nemůže začínat číslicí:

user=> '(1 (2 (3 (4 5))))
(1 (2 (3 (4 5))))

Následuje ukázka zápisu vektoru. Vzhledem k tomu, že v případě vektorů lze velmi efektivně přistupovat k jeho prvkům pomocí indexů, podobají se vektory javovskému typu pole/array:

user=> [ [1 2 3] [4 5 6] [7 8 9]]
[[1 2 3] [4 5 6] [7 8 9]]

Všechny kolekce lze samozřejmě různým způsobem kombinovat; například zde je uveden vektor seznamů:

user=> [ '(:jedna :dva) '(:tri :ctyri) '(:pet :sest) ]
[(:jedna :dva) (:tri :ctyri) (:pet :sest)]

Mapa, popř. též asociativní pole, se v reálných aplikacích používá poměrně často, neboť odstraňuje některé nevýhody seznamů i vektorů:

user=> {:prvni 1, :druhy 2}
{:druhy 2, :prvni 1}

Zbývají nám již jen množiny:

user=> #{"toto" "je" "mnozina"}
#{"mnozina" "toto" "je"}

Příklad volání funkcí * (součin) a + (součet):

user=> (* (+ 1 2 3) (+ 3 4))
42

Příklad použití speciální formy if:

; na základě podmínky se vyhodnotí (a vrátí jako výsledek)
; buď řetězec "mensi" nebo "vetsi"
(if (< 1 2) "mensi" "vetsi")
"mensi"

V tomto případě je zaručeno, že se na základě podmínky – tedy prvního parametru speciální formy if – vyhodnotí buď druhý parametr nebo třetí parametr, nikoli však oba parametry současně. Právě z tohoto důvodu je if implementován jako speciální forma a nikoli jako běžná složená forma – funkce.

2. Speciální forma def

První speciální formou, s níž se dnes seznámíme, je speciální forma nazvaná def. Ta se většinou používá k navázání libovolné hodnoty (například čísla, pravdivostní hodnoty, řetězce, seznamu a jak uvidíme dále, tak i funkce) na symbol. Méně časté je použití této speciální formy k pouhému vytvoření symbolu. V případě, že Clojure vyhodnotí („spustí“) tuto speciální formu, dojde k vytvoření nové globální proměnné v aktuálně nastaveném jmenném prostoru (nejde tedy o skutečnou globální proměnnou, ale o proměnnou identifikovatelnou přes jmenný prostor – viz dále) a k inicializaci této proměnné. Pokud již globální proměnná stejného jména existuje, dojde k „přepisu“ její hodnoty. Ve skutečnosti však stará hodnota nemusí přestat existovat, protože může být navázána na další proměnné. Navíc je možné pomocí nepovinného parametru k proměnné přiřadit i dokumentační řetězec; jak se to dělá, si ukážeme v jednom demonstračním příkladu.

Poznámka: Clojure nám sice bez jakéhokoli varování umožní změnit hodnotu globální proměnné s využitím formy def, ovšem v praxi se tento obrat nedoporučuje používat, protože změna těchto proměnných obecně není ve vícevláknových programech bezpečná a programová změna globálních proměnných je z tohoto důvodu považována za špatný návyk. Naproti tomu manuální změna je možná a někdy i nutná.

Podívejme se nyní na demonstrační příklady, na nichž je ukázán základní způsob použití speciální formy def:

Vytvoření nové globální proměnné nazvané message a její použití ve funkci println (co tato funkce dělá pravděpodobně není zapotřebí podrobně vysvětlovat :-):

user=> (def message "Hello world")
#'user/message
 
user=> (println message)
Hello world
nil

Vytvoření globálních proměnných x a y s jejich inicializací pomocí číselného literálu. Posléze se vytvoří třetí globální proměnná nazvaná answer a naplní se výsledkem vyhodnocené formy (* x y). Následně je hodnota globální proměnné answer vytištěna, protože jméno proměnné se vždy vyhodnotí na její hodnotu:

user=> (def x 6)
#'user/x
 
user=> (def y 7)
#'user/y
 
user=> (def answer (* x y))
#'user/answer
 
user=> answer
42

Jak jsme se již řekli v úvodním odstavci této kapitoly, je možné ke globální proměnné přiřadit i takzvaný dokumentační řetězec:

user=> (def answer "Odpoved na otazku o ... vesmiru, zivote a vubec" 42)
#'user/answer

Tento dokumentační řetězec lze kdykoli zobrazit s využitím makra doc:

user=> (doc answer)
-------------------------
user/answer
  Odpoved na otazku o ... vesmiru, zivote a vubec

Bližší informace o tomto makru podá samozřejmě příkaz:

(doc doc)
-------------------------
clojure.repl/doc
([name])
Macro
  Prints documentation for a var or special form given its name

3. Jmenné prostory

V předchozí kapitole jsme se poprvé explicitně zmínili o takzvaných jmenných prostorech. Jmenné prostory byly do programovacího jazyka Clojure přidány zejména z toho důvodu, že použití globálních symbolů je v reálných programech velmi nebezpečné a to zejména proto, že jiná část programu, která může být klidně vytvořena i jiným vývojářem, může nechtěně předeklarovat již existující globální symbol. Co je ještě horší – tato předeklarace nemusí nutně vést k okamžité chybě při práci s programem (ideálně při jeho testování), ale může se projevit až při určité shodě okolností – podle všeobecně platného zákona tedy ve chvíli, kdy se aplikace předvádí šéfovi či zákazníkovi :-). Připomeňme si, že mezi globální symboly patří i symboly představující jména funkcí, takže je asi představitelné, co by se stalo, kdyby nějaká importovaná knihovna náhodou obsahovala funkci pojmenovanou stejně, jako funkce vytvořená programátorem vyvíjené aplikace. Jmenné prostory proto představují jeden z možných způsobů, jak tento problém poměrně elegantně vyřešit.

My jsme se již vlastně s jedním jmenným prostorem setkali v textech vypisovaných smyčkou REPL, i když jsme si prozatím nevysvětlili, že se skutečně jedná o jmenný symbol. Při používání smyčky REPL je totiž jméno aktuálního jmenného prostoru vypisováno jako součást výzvy (prompt):

user=>

V programovacím jazyku Clojure je možné vytvořit takřka libovolný počet jmenných prostorů a posléze se mezi těmito jmennými prostory přepínat, tj. lze zvolit, který jmenný prostor bude jmenným prostorem aktuálním. Pro tuto činnost se používá makro nazvané ns:

(ns název_jmenného_prostoru)

Podívejme se nyní na jednoduchý demonstrační příklad, v němž jsou vytvořeny dvě globální proměnné nazvané answer. Každé proměnné je přiřazena jiná hodnota a každá proměnná tudíž musí být uložena v jiném jmenném prostoru. Povšimněte si taktéž toho, jak se změní výzva (prompt) při přepnutí aktuálního jmenného prostoru:

; vytvoření globální proměnné umístěné ve jmenném prostoru "user"
user=> (def answer 42)
#'user/answer
 
; jméno proměnné se vyhodnotí na hodnotu proměnné
user=> answer
42
 
; vytvoření nového jmenného prostoru nazvaného "novy"
user=> (ns novy)
nil
 
; lze v tomto jmenném prostoru vyhodnotit (=najít) proměnnou answer?
novy=> answer
CompilerException java.lang.RuntimeException: Unable to resolve symbol: answer
in this context, compiling:(NO_SOURCE_PATH:0)
 
; vytvoření nové globální proměnné ve jmenném prostoru "novy"
novy=> (def answer "?")
#'novy/answer
 
; její hodnotu nyní můžeme získat (vyhodnotit), aniž by došlo k chybě
novy=> answer
"?"
 
; přepnutí jmenného prostoru
novy=> (ns user)
nil
 
; nyní je opět viditelná první globální proměnná se jménem answer
user=> answer
42

Ve skutečnosti však nejsou jednotlivé jmenné prostory od sebe izolovány, takže se můžeme odkazovat na symbol umístěný v jiném jmenném prostoru pomocí zápisu jmenný_prostor/symbol. Ostatně i kvůli podpoře tohoto způsobu zápisu není možné použít znak / ve jméně žádného symbolu (znaky * či – je však možné použít, což se taktéž často děje, protože – se používá pro oddělení jednotlivých slov v názvu symbolu a hvězdička je podle konvencí používána pro konstanty). Podívejme se nyní na způsob využití zápisu jmenný_prostor/symbol:

; přepnutí jmenného prostoru
user=> (ns user)
nil
 
; proměnná z aktuálního jmenného prostoru
user=> answer
42
 
; proměnná z jiného jmenného prostoru
user=> novy/answer
"?"
 
; přepnutí jmenného prostoru
user=> (ns novy)
nil
 
; proměnná z aktuálního jmenného prostoru
novy=> answer
"?"
 
; proměnná z aktuálního jmenného prostoru
novy=> novy/answer
"?"
 
; proměnná z jiného jmenného prostoru
novy=> user/answer
42

4. Role a vlastnosti funkcí ve funkcionálních programovacích jazycích

V dnešním článku se budeme zabývat především popisem tvorby uživatelských funkcí v programovacím jazyku Clojure. Připomeňme si, že Clojure patří mezi funkcionální jazyky, které se kromě dalších zajímavých vlastností vyznačují i tím, že funkce jsou v nich zpracovávány prakticky stejným způsobem, jako jiné hodnoty, například celá čísla či řetězce. Pro tuto vlastnost funkcionálních programovacích jazyků se většinou používá termín first-class functions, ale již méně často se vysvětluje, co vlastně tento termín v daném programovacím jazyku znamená. Ukažme si pro začátek, jakým způsobem se vlastně pracuje s běžnými hodnotami, a to nikoli ve funkcionálním jazyku, ale v jazyku objektově orientovaném – v Javě. V následujícím demonstračním příkladu je použita celočíselná hodnota 42, která je využita hned několika různými způsoby:

public class Test {
 
    int testMethod(int param) {
        // hodnotu je možné uložit do proměnné (navázat na jméno)
        int i = 42;
 
        // hodnotu je možné použít ve výrazu
        // (zde není na žádné jméno navázána)
        int j = param * i * 42;
 
        // hodnotu je možné předat volané metodě (funkci)
        j += Math.max(42, param);
 
        // hodnotu je možné vrátit při ukončení metody (funkce)
        return 42;
    }
 
    public static void main(String[] args) {
        // hodnotu je možné předat jako parametr metody (funkce)
        new Test().testMethod(42);
    }
}

Tento způsob práce s hodnotami mají prakticky všichni programátoři zažitý a vlastně je vůbec nepřekvapuje. Již menší množství vývojářů je však seznámeno s tím, že ve funkcionálních jazycích je možné namísto slova hodnota použít i slovo funkce, protože funkce jsou, jak již víme, v těchto jazycích plnohodnotnými hodnotami. S vědomím toho, jak se s hodnotami zacházelo v předchozím demonstračním příkladu a toho, že ve funkcionálních jazycích lze s funkcemi pracovat stejně jako s jinými hodnotami, tedy lze říci, že funkce mají mj. i tyto vlastnosti:

  1. Funkce mohou být vytvořeny kdykoli v čase běhu programu (runtime). To, jak je funkce v runtime přeložena, je již interní záležitostí daného programovacího jazyka.
  2. Funkce mohou být uloženy do proměnné (resp. přesněji řečeno navázány na jméno proměnné) či mohou být uloženy do jakékoli datové struktury (seznam funkcí, vektor funkcí atd.)
  3. Funkce mohou být předány jako parametr do jiné funkce.
  4. Funkce může být vrácena ve formě návratové hodnoty jiné funkce.
  5. Jméno sice není součástí funkce, ovšem funkce může být na jméno (symbol) navázána. Toto je pravděpodobně jedna z nejvíce matoucích vlastností funkcionálních jazyků, protože z jazyků procedurálních jsme zvyklí na to, že funkce vždy jméno, které je v naprosté většině případů známé již v době překladu. U jazyků funkcionálních (popř. jazyků hybridních) se však velmi často používají funkce beze jména, neboli funkce anonymní.

5. Vytvoření uživatelských funkcí

Již známe alespoň základní vlastnosti funkcí v Clojure, jak se však funkce v tomto programovacím jazyku skutečně definují? Pro vytvoření nové bezejmenné (tj. anonymní) funkce se používá speciální forma nazvaná fn, které se v tom nejjednodušším případě předá vektor obsahující jména parametrů, za nímž je uveden seznam, jenž představuje tělo funkce (znalci LISPu patrně znají formu lambda, která má podobný význam). Samozřejmě, že v těle funkce je možné použít symbolická jména jejích parametrů a návratovou hodnotou funkce je hodnota získaná vyhodnocením těla funkce. Speciální forma fn při použití ve smyčce REPL vypíše řetězec, který reprezentuje interní identifikátor funkce – jinými slovy na tento řetězec můžeme v naprosté většině případů zapomenout, protože se s ním přímo nepracuje. Ukažme si tedy způsob deklarace funkce se dvěma parametry pojmenovanými x a y, která vypočítá a vrátí součin těchto parametrů:

user=> (fn [x y] (* x y))
#<user$eval46$fn__47 user$eval46$fn__47@1697b67>

Co se vlastně stalo? Vytvořili jsme novou funkci, která však nebyla přiřazena k žádnému symbolu (tj. nebyla „pojmenována“) ani jsme tuto funkci nikde nezavolali. Výše uvedený zápis je tedy prakticky stejně užitečný, jako prosté zapsání jakékoli hodnoty nebo symbolu na vstup smyčky REPL. Pokud by se funkce měla zavolat, lze použít nám již známý zápis ve tvaru seznamu – již víme, že prvním parametrem vyhodnocovaného seznamu (není před ním apostrof!) je funkce a dalšími prvky pak parametry této funkce:

user=> ((fn [x y] (* x y)) 6 7)
42

Sice je pěkné, že jsme dokázali funkci zavolat s předáním parametrů, ovšem mnohdy (ne vždy!) je nutné funkci „pojmenovat“, přesněji řečeno ji přiřadit k symbolu. My vlastně již víme, jak se to dělá, protože funkce jsou hodnotami a pro přiřazení symbolu k hodnotě se používá speciální forma def. Tudíž následující zápis je sice zdlouhavý, ale zcela korektní:

user=> (def multiply (fn [x y] (* x y)))
#'user/multiply

Předchozím příkazem jsme vytvořili novou funkci a navázali ji na symbol, tudíž došlo k jejímu pojmenování. Nyní je již možné funkci zavolat s využitím navázaného symbolu. Samozřejmě se zde opět využívá nám již známý zápis ve tvaru seznamu:

user=> (multiply 7 8)
56

6. Zjednodušená deklarace funkcí, funkce s více aritami

Vzhledem k tomu, že se uživatelské funkce v reálných programech vytváří a současně i pojmenovávají velmi často, vznikla potřeba nahradit zápis (def název (fn parametry (tělo))) něčím kratším, ideálně i s použitím menšího množství závorek. Pro tyto účely vzniklo makro se jménem defn, které se až na malé detaily podobá LISPovskému zápisu defun. Při použití makra defn se v tom nejjednodušším případě předávají tři parametry: název nově vytvářené funkce, vektor obsahující jména parametrů funkce a konečně seznam představující tělo této funkce. Naši funkci multiply tedy můžeme vytvořit a současně i pojmenovat následujícím způsobem:

user=> (defn multiply [x y] (* x y))
#'user/multiply

A ihned ji můžeme použít:

user=> (multiply 6 7)
42

Zbývá jen dodat, že nově vytvořená funkce je polymorfní vzhledem k typům parametrů, protože se její chování liší v závislosti na tom, jaké hodnoty jsou funkci v čase běhu programu předány. Ukažme si to na příkladech tří typů numerických hodnot: celých čísel, zlomků a hodnot typu BigDecimal:

user=> (multiply 6 7)
42
user=> (multiply 1/2 1/3)
1/6
user=> (multiply 10000000M 2000000M)
20000000000000M
user=>

V programovacím jazyku Clojure lze vytvářet i funkce s proměnným počtem parametrů (ukážeme si příště) a s proměnnou aritou. Jen pro zajímavost se nyní podívejme na způsob, jakým se vytvoří funkce multiply, kterou lze volat s jedním parametrem, se dvěma parametry popř. alternativně se třemi parametry. Tělo této funkce je pokaždé odlišné:

user=> (defn multiply ([x] (* x x)) ([x y] (* x y)) ([x y z] (* x y z)))
#'user/multiply

Výše popsaný způsob zápisu není moc čitelný, proto si ještě jednou funkci multiply rozepíšeme tak, jak je to v Clojure/LISPu zvykem:

(defn multiply
    ([x]
     (* x x))
    ([x y]
     (* x y))
    ([x y z] (* x y z)))

Vidíme, že vektor parametrů i těla jednotlivých variant jsou uloženy ve zvláštním seznamu.

Můžeme se pustit do testování:

; volání varianty funkce multiply s jedním parametrem
user=> (multiply 6)
36
 
; volání varianty funkce multiply se dvěma parametry
user=> (multiply 6 7)
42
 
; volání varianty funkce multiply se třemi parametry
user=> (multiply 6 7 8)
336
 
; použití čtyř parametrů povede k chybě při pokusu o vyhodnocení formy
user=> (multiply 6 7 8 9)
ArityException Wrong number of args (4) passed to: user$multiply  clojure.lang.A
Fn.throwArity (AFn.java:437)

7. Programové smyčky ve funkcionálních jazycích? Nikoli, spíše rekurze!

Dostáváme se k další, možná poněkud kontroverzní vlastnosti programovacího jazyka Clojure. Tento jazyk totiž, podobně jako mnohé další programovací jazyky, preferuje rekurzi před masivním používáním programových smyček. Jsou pro to samozřejmě dobré důvody, jak teoretické, tak i praktické (opět jde o paralelní výpočty). Ve skutečnosti je však Clojure jazykem orientovaným na praktické použití, takže ve skutečnosti obsahuje „nefunkcionální“ smyčku while (samozřejmě nejde o příkaz, ale v tomto případě o makro), popř. o ryze „funkcionální“ makro for používané při zpracování seznamů. Toto makro je všestranné a současně i velmi užitečné, proto se s ním příště seznámíme podrobněji. Vraťme se však k rekurzi. V Clojure lze většinou použít přímý zápis rekurze, tj. v těle vytvářené funkce se může objevit volání této funkce. Zcela typickým příkladem rekurzivní funkce je funkce pro výpočet faktoriálu, jejíž jednoduchá varianta (neochráněná před všemi typy vstupů) může vypadat takto:

user=> (defn fact [n] (if (<= n 1) 1 (* n (fact (- n 1)))))
#'user/fact

Pokud jste se – stejně jako já – ztratili v závorkách, použijeme místo málo čitelného one-lineru strukturovanější zápis, kde je zřejmé, jak je funkce vytvořena:

(defn fact
    [n]
    (if (<= n 1)
        1
        (* n (fact (- n 1)))))

Ihned můžeme provést jednoduchý test:

user=> (fact 0)
0
user=> (fact 1)
1
user=> (fact 10)
3628800

Faktoriál se však nemusí počítat pouze s celými čísly reprezentovanými interně jako hodnoty typu int. Můžeme využít i již vícekrát zmíněného faktu, že Clojure dokáže provádět výpočty i s hodnotami, které v programovacím jazyku Java odpovídají instancím tříd java.math.BigInteger a java.math.BigDecimal. Můžeme se například pokusit vypočítat 100!. Zde je již nutné v Clojure použít za konstantou znak M, aby se výpočet neprováděl pouze s čísly typu int:

user=> (fact 100M)
93326215443944152681699238856266700490715968264381621468592963895217599993229915
608941463976156518286253697920827223758251185210916864000000000000000000000000M

Přílišnému nadšení nad tím, jak jednoduše nyní můžeme počítat faktoriál z libovolně velkého čísla, však nepodléhejme, protože například již pro 10000! dojde k nepříjemnému překvapení:

user=> (fact 10000M)
StackOverflowError   clojure.lang.PersistentHashMap$BitmapIndexedNode.index (Per
sistentHashMap.java:510)

Důvodem vedoucím ke vzniku této chyby i způsobem její nápravy se budeme zabývat v následující kapitole.

8. Možné problémy s rekurzí a řešení těchto problémů – tail rekurze

Důvod, proč předchozí volání funkce fact skončilo s chybou, spočívá v tom, že došlo k přeplnění zásobníku při rekurzivním volání. Na zásobník se totiž musí ukládat parametry předávané volané funkci a taktéž body návratu (zjednodušeně řečeno návratové adresy). Aby k přetečení zásobníku nedocházelo, můžeme naši funkci fact upravit tak, aby se využívalo takzvané tail rekurze. Velmi zjednodušeně řečeno je tail rekurze použita tehdy, pokud je posledním příkazem nějaké funkce příkaz pro rekurzivní volání té samé funkce. V tomto případě se nemusí na zásobník nic ukládat a namísto toho se prostě provede skok. V Clojure se však musí tail rekurze zapsat explicitně, což má své přednosti i zápory (podle mě převažují přednosti, protože již ze zápisu programu je zcela zřejmé, kdy k tail rekurzi skutečně dojde).

Na základě informací, které jsme se dozvěděli v předchozím textu, se tedy pokusme upravit původní čistě rekurzivní způsob zápisu funkce pro výpočet faktoriálu takovým způsobem, aby bylo možné využít tail rekurzi. Funkci je nutné upravit tak, aby jejím posledním příkazem bylo opět volání fact – u původní verze tomu tak nebylo, protože posledním příkazem bylo násobení. První pokus o úpravu spočívá v zavedení akumulátoru výsledku:

(defn fact
    [n acc]
    (if (<= n 1)
        acc
        (fact (- n 1) (* acc n))))

Zápis můžeme zjednodušit náhradou (- n 1) za (dec n):

(defn fact
    [n acc]
    (if (<= n 1)
        acc
        (fact (dec n) (* acc n))))

Ovšem stále je zde jeden problém – z původní funkce s jedním parametrem se nyní stala funkce, jíž je nutné předávat i druhý parametr, který navíc musí být nastavený na jedničku. Náprava je prostá a spočívá v použití funkce fact s volitelnou aritou (popř. by bylo možné vytvořit pomocnou funkci, ovšem mě se následující zápis líbí více, protože se zbytečně nevytváří pomocné funkce):

(defn fact
    ([n]
     (fact n 1))
    ([n acc]
     (if (<= n 1)
         acc
         (fact (dec n) (* acc n)))))

Výše uvedená funkce již může využívat tail rekurze, ovšem jak jsme si již řekli, je nutné tail rekurzi zapsat explicitně. Proto i zde dojde k chybě při přetečení zásobníku (na rozdíl od mnoha LISPů):

user=> (fact 10000M)
StackOverflowError   clojure.lang.PersistentHashMap$BitmapIndexedNode.index (Per
sistentHashMap.java:510)

Explicitní zápis rekurze spočívá ve využití speciální formy recur, která se zapíše přesně do místa, kde má k tail rekurzi (=skoku) dojít:

bitcoin_skoleni

(defn fact
    ([n]
     (fact n 1))
    ([n acc]
     (if (<= n 1)
         acc
         (recur (dec n) (* acc n)))))

Pokud máte dostatek paměti a trpělivosti, můžete zadat tento příkaz a zjistit, zda dojde či nedojde k přetečení zásobníku:

user=> (fact 10000M)

Mohlo by se zdát, že by překladač programovacího jazyka Clojure snad bylo možné zmást a použít recur jako náhradu skoku kdekoli. Ve skutečnosti si však překladač hlídá, zda je recur použito správně:

(defn fact
    [n]
    (if (<= n 1)
        1
        (* n (recur (- n 1)))))
CompilerException java.lang.UnsupportedOperationException: Can only recur from t
ail position, compiling:(NO_SOURCE_PATH:24)

Pokud vám použití rekurze či tail rekurze z nějakého důvodu vadí, vězte, že ve skutečně velkém množství případů je řešením použití již zmíněného makra for, které si představíme příště.

9. Odkazy na Internetu

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