Obsah
1. Řídicí struktury využitelné v programovacím jazyku Clojure (dokončení)
3. Vyhodnocování všech větví makrem cond->
7. Jedno z nejsložitějších maker ze standardní knihovny: case
8. Větší množství konstant u jednotlivých větví v makru case
9. Kdy použít cond, condp a case?
10. Expanze makra case a forma vygenerovaného bajtkódu
11. Makra ze jmenného prostoru clojure.core.logic
12. Od „jednorozměrných“ rozhodovacích konstrukcí ke konstrukcím dvourozměrným
14. Příklady použití makra cond-table
16. A na konci je speciální forma if
17. Příloha: zdrojové kódy výše popsaných maker
18. Repositář s demonstračními příklady
19. Odkazy na předchozí části seriálu o programovacím jazyku Clojure
1. Řídicí struktury využitelné v programovacím jazyku Clojure (dokončení)
V dnešním článku dokončíme téma, kterému jsme se věnovali minule. Nejprve si popíšeme některá další (a nutno říci, že velmi často používaná) makra nabízená standardní knihovnou programovacího jazyka Clojure. Jedná se především o makra cond->, cond->> (což jsou varianty makra cond, které již dobře známe), dále pak o makro if-not a v neposlední řadě o interně dosti složité makro nazvané příhodně case. To však není zdaleka vše, protože existují i další makra určená pro řízení toku programu. Především se jedná o makra nabízená ve jmenném prostoru clojure.core.logic, zejména o makra nazvaná conde, condu a conda. Asi nebude velkým překvapením, že se opět jedná o varianty standardního makra cond.
Ještě zajímavější jsou však makra z vybraných nestandardních knihoven, zejména pak velmi povedené makro cond-table, jehož autorem je Daniel Gregoire. S využitím cond-table je možné vytvářet i složité rozhodovací tabulky (což jsou de facto dvourozměrné rozhodovací struktury, které možnosti jednorozměrných struktur dosti podstatným způsobem zobecňují). Později se zmíníme i o složitějších rozhodovacích strukturách nabízených například knihovnou special, better-cond, cond-let atd.
# | Konstrukce | Článek/kapitola | Typ | Stručný popis |
---|---|---|---|---|
1 | if | 1/2 | speciální forma | základní rozhodovací konstrukce, základ pro další makra |
2 | if-not | makro | stejné jako speciální forma if, ovšem s negovanou podmínkou | |
3 | if+do | 1/3 | dvě speciální formy | použito ve chvíli, kdy je nutné do jedné větve či obou větví zapsat více výrazů |
4 | if-let | 1/5 | makro | kombinace speciálních forem if a let |
5 | if-some | 1/6 | makro | kombinace speciálních forem if (test na nil) a let |
6 | and | 1/7 | makro | postupné vyhodnocování předaných výrazů až do chvíle, kdy se vrátí nil či false |
7 | or | 1/7 | makro | postupné vyhodnocování předaných výrazů až do chvíle, kdy se vrátí true |
8 | when | 1/4 | makro | vhodná náhrada za if s jedinou větví s více výrazy |
9 | when-not | 1/4 | makro | vhodná náhrada za if-not s jedinou větví s více výrazy |
10 | when-let | 1/5 | makro | kombinace speciálních forem when a let |
11 | when-some | 1/6 | makro | kombinace speciálních forem when (test na nil) a let |
12 | when-first | makro | použito při testu prvního prvku sekvence s následným zpracováním sekvence | |
13 | cond | 1/8 | makro | postupné testování podmínek, pokud je podmínka splněna, vrátí se hodnota příslušného výrazu |
14 | cond + :else | 1/8 | makro | typické použití makra cond s větví :else nebo :default |
15 | condp | 1/10 | makro | postupné dosazování testované hodnoty do zadaného výrazu, obdoba switch-case |
16 | conde | 2/11 | makro | makro z knihovny clojure.core.logic (logické programování) |
17 | condu | 2/11 | makro | makro z knihovny clojure.core.logic (logické programování) |
18 | conda | 2/11 | makro | makro z knihovny clojure.core.logic (logické programování) |
19 | cond-> | 2/2 | makro | odvozeno od cond a threading makra -> |
20 | cond->> | 2/5 | makro | odvozeno od cond a threading makra -> |
21 | case | 2/7 | makro | rozeskok na základě porovnání hodnoty výrazu s konstantami |
22 | cond-table | 2/13 | makro | nová nestandardní konstrukce |
součást zdrojového kódu (zapisovaná do smyčky REPL) zvýraznění právě popisovaného makra výsledná hodnota, popř. text vytištěný funkcemi print, println atd.
2. Makro cond->
První makro s nímž se v dnešním článku setkáme a které nám umožňuje zapsat rozvětvení na základě vyhodnocovaných podmínek, se jmenuje cond->. Jedná se o makro ze standardní knihovny (ta je automaticky dostupná), takže si můžeme ihned vypsat nápovědu k tomuto makru:
(doc cond->) ------------------------- clojure.core/cond-> ([expr & clauses]) Macro Takes an expression and a set of test/form pairs. Threads expr (via -&) through each form for which the corresponding test expression is true. Note that, unlike cond branching, cond-> threading does not short circuit after the first true test expression.
Toto na první pohled poněkud neobvyklé pojmenování nám naznačuje, že se bude jednat o konstrukci, která je založena na takzvaném threading makru. Samotné threading makro je přitom jednou z nejelegantnějších konstrukcí programovacího jazyka Clojure vůbec:
(doc ->) ------------------------- clojure.core/-> ([x & forms]) Macro Threads the expr through the forms. Inserts x as the second item in the first form, making a list of it if it is not a list already. If there are more forms, inserts the first form as the second item in second form, etc.
Toto makro nám umožňuje zapisovat sérii funkcí, mezi nimiž se předávají parametry podobně, jako je tomu v klasické koloně (pipe). Samotný zápis je přitom velmi stručný a obejde se bez řady závorek, které by jinak bylo nutné použít v případě NEpoužití threading makra:
(-> 1 inc println) 2
Další jednoduchý příklad:
(-> (range 1 10) count println) 9
Volat je ovšem možné i funkce s větším množstvím parametrů, včetně metod:
(-> "Hello world." .toUpperCase (.replace "." "!") println) HELLO WORLD!
Popř.:
(-> "Hello world." .toUpperCase (.replace "." "!") (.split " ") first println) HELLO
Expanze tohoto makra vede k zápisu s velkým množstvím závorek tak typickým pro klasické LISPovské jazyky bez tohoto makra:
(macroexpand '(-> "Hello world." .toUpperCase (.replace "." "!") (.split " ") first println))
S výsledkem:
(println (first (.split (.replace (.toUpperCase "Hello world.") "." "!") " ")))
Vraťme se však k makru cond->, které je odvozené od makra cond. Jeho činnost je následující: postupně jsou testovány jednotlivé podmínky (sudé parametry) a pokud je podmínka splněna, je do výrazu za ní (liché parametry) dosazen první parametr makra, podobně jako je tomu u výše zmíněného threading makra (tedy na první pozici ve volané funkci či metodě).
V následujícím úryvku kódu se pokoušíme převést obsah řetězce na číselnou hodnotu, ovšem s testem, zda je vstupní hodnota skutečně řetězcem. S využitím makra cond-> se samotné x (tedy reference na převáděnou hodnotu) nemusí opakovat v podmínce a současně i v převodní funkci:
(let [x "42"] (cond-> x (string? x) (Integer.))) 42
Poněkud umělý příklad, který je na makru cond-> postaven, může testovat hodnotu parametru x a následně na základě hodnoty tohoto parametru provádí operace (inc x), (/ x 2) a (dec x):
(defn machina [x] (cond-> x (odd? x) inc (even? x) (/ 2) (zero? x) dec))
Výše uvedenou funkci si otestujeme na vstupních hodnotách od nuly do deseti:
(doseq [x (range 0 11)] (println x (machina x)))
S těmito výsledky:
0 -1 1 2 2 1 3 4 4 2 5 6 6 3 7 8 8 4 9 10 10 5
3. Vyhodnocování všech větví makrem cond->
Na rozdíl od makra cond, které při splnění nějaké podmínky z celé sady již další podmínky netestuje, je tomu u makra cond-> odlišně, protože se postupně prochází všechny podmínky. To může vést k chybám, zejména tehdy, pokud se použije větev :else (symbol :else se totiž vždy vyhodnotí na pravdu). Pokud tedy předchozí příklad nepatrně upravíme:
(defn machina-2 [x] (cond-> x (odd? x) inc (even? x) (/ 2) :else dec))
Získáme odlišné výsledky, což si ostatně můžeme velmi snadno ukázat:
(doseq [x (range 0 11)] (println x (machina x) (machina-2 x)))
Tabulka vypsaná předchozí smyčkou zobrazuje postupně vstupní hodnotu, výpočet provedený první funkcí machina a výpočet provedený funkcí upravenou machina-2:
0 -1 -1 1 2 1 2 1 0 3 4 3 4 2 1 5 6 5 6 3 2 7 8 7 8 4 3 9 10 9 10 5 4
Ještě lépe je toto chování patrné v případě, že se v jednotlivých větvích nachází funkce s vedlejším efektem, zde konkrétně funkce println:
(defn machina-3 [x] (cond-> x (odd? x) (println "odd") (even? x) (println "even") :else (println "zero")))
Zkusme si tuto funkci vyvolat postupně s hodnotami od 0 do 10:
(doseq [x (range 0 11)] (println x) (machina-3 x) (println))
Z vypsaných výsledků je patrné, že se vždy vyhodnotí i poslední větev:
0 0 even nil zero 1 1 odd nil zero 2 2 even nil zero 3 3 odd nil zero 4 4 even nil zero 5 5 odd nil zero 6 6 even nil zero 7 7 odd nil zero 8 8 even nil zero 9 9 odd nil zero 10 10 even nil zero
Makro cond-> je tedy většinou vhodné použít pouze tehdy, pokud se množiny hodnot, pro které podmínky platí, nepřekrývají.
4. Expanze makra cond->
Otestovat si můžeme i expanzi makra cond->. Pro výpis expandovaného makra opět použijeme známou funkci macroexpand ze standardní knihovny programovacího jazyka Clojure:
(macroexpand '(cond-> x (odd? x) inc (even? x) (/ 2) (zero? x) inc))
Výsledkem by měl být následující kód (pomocná automaticky vygenerovaná proměnná může mít ovšem odlišné jméno):
(let* [G__10086 x G__10086 (if (odd? x) (clojure.core/-> G__10086 inc) G__10086) G__10086 (if (even? x) (clojure.core/-> G__10086 (/ 2)) G__10086)] (if (zero? x) (clojure.core/-> G__10086 inc) G__10086))
Příklad expanze ne plně funkčního kódu s větví else:
(macroexpand '(cond-> x (odd? x) inc (even? x) (/ 2) :else inc))
Nyní bude expanze vypadat následovně:
(let* [G__10117 x G__10117 (if (odd? x) (clojure.core/-> G__10117 inc) G__10117) G__10117 (if (even? x) (clojure.core/-> G__10117 (/ 2)) G__10117)] (if :else (clojure.core/-> G__10117 inc) G__10117))
Extrémní příklad, v němž se provedou všechny větve a současně se mezivýsledek pětkrát zvětší o jedničku:
(cond-> 1 :foo inc :bar inc :baz inc :else inc :default inc) 6
Bude expandován takto:
(macroexpand '(cond-> 1 :foo inc :bar inc :baz inc :else inc :default inc)) (let* [G__11216 1 G__11216 (if :foo (clojure.core/-> G__11216 inc) G__11216) G__11216 (if :bar (clojure.core/-> G__11216 inc) G__11216) G__11216 (if :baz (clojure.core/-> G__11216 inc) G__11216) G__11216 (if :else (clojure.core/-> G__11216 inc) G__11216)] (if :default (clojure.core/-> G__11216 inc) G__11216))
5. Makro cond->>
Druhé dnes popisované makro určené pro implementaci rozvětvení na základě zadaných podmínek se jmenuje cond->>:
(doc cond->>) ------------------------- clojure.core/cond->> ([expr & clauses]) Macro Takes an expression and a set of test/form pairs. Threads expr (via ->>) through each form for which the corresponding test expression is true. Note that, unlike cond branching, cond->> threading does not short circuit after the first true test expression.
Opět se jedná o variantu makra cond, tentokrát ovšem zkombinovanou s alternativní formou threading makra ->>:
(doc ->>) ------------------------- clojure.core/->> ([x & forms]) Macro Threads the expr through the forms. Inserts x as the last item in the first form, making a list of it if it is not a list already. If there are more forms, inserts the first form as the last item in second form, etc.
(->> (range 1 11) (filter even?)) (2 4 6 8 10)
Součet všech sudých čísel vybraných z řady 0..10:
(->> (range 1 11) (filter even?) (reduce +)) 30
Vraťme se k makru cond->>. Na rozdíl od výše popsaného makra cond-> se liší dosazování parametru do jednotlivých větví – parametr je dosazen na poslední místo výrazu ve větvi (ta je typicky představována voláním nějaké funkce) a nikoli na místo první. Pokud tedy například použijeme tuto funkci:
(defn machina-3 [x] (cond->> x (odd? x) inc (even? x) (/ 2)))
bude se ve druhé větvi vyhodnocovat funkce (/ 2 x) a nikoli (/ x 2).
Výše definovanou funkci machina-3 si otestujeme na vstupních hodnotách od nuly do deseti:
(doseq [x (range 1 11)] (println x (machina-3 x)))
S výsledkem:
1 2 2 1 3 4 4 1/2 5 6 6 1/3 7 8 8 1/4 9 10 10 1/5
Nepatrně složitější příklad, který se snaží převést parametr na celé číslo:
(defn ->int [x] (cond->> x (string? x) Integer. (float? x) int (rational? x) int (integer? x) identity))
Chování této funkce si můžeme snadno otestovat:
(println (->int 3.14)) 3 (println (->int 10/3)) 3 (println (->int 42)) 42 (println (->int "1000")) 1000
6. Expanze makra cond->>
Způsob expanze makra cond->> si ukážeme na příkladu převodníku hodnoty typu řetězec, čísla s plovoucí řádovou čárkou, zlomku či celého čísla na hodnotu typu integer (tedy celé číslo). Expanzi si opět zobrazíme standardní funkcí macroexpand popř. macroexpand-1:
(macroexpand '(cond->> x (string? x) Integer. (float? x) int (rational? x) int (integer? x) identity))
Výsledek expanze by měl v tomto případě vypadat následovně:
(let* [G__10237 x G__10237 (if (string? x) (clojure.core/->> G__10237 Integer.) G__10237) G__10237 (if (float? x) (clojure.core/->> G__10237 int) G__10237) G__10237 (if (rational? x) (clojure.core/->> G__10237 int) G__10237)] (if (integer? x) (clojure.core/->> G__10237 identity) G__10237))
7. Jedno z nejsložitějších maker ze standardní knihovny: case
Interně nejsložitější makro ze standardní knihovny, které si dnes popíšeme, se jmenuje case:
(doc case) ------------------------- clojure.core/case ([e & clauses]) Macro Takes an expression, and a set of clauses. Each clause can take the form of either: test-constant result-expr (test-constant1 ... test-constantN) result-expr The test-constants are not evaluated. They must be compile-time literals, and need not be quoted. If the expression is equal to a test-constant, the corresponding result-expr is returned. A single default expression can follow the clauses, and its value will be returned if no clause matches. If no default expression is provided and no clause matches, an IllegalArgumentException is thrown. Unlike cond and condp, case does a constant-time dispatch, the clauses are not considered sequentially. All manner of constant expressions are acceptable in case, including numbers, strings, symbols, keywords, and (Clojure) composites thereof. Note that since lists are used to group multiple constants that map to the same expression, a vector can be used to match a list if needed. The test-constants need not be all of the same type.
Toto makro se do značné míry podobá céčkové či javovské konstrukci switch, protože výsledek zadaného výrazu je porovnáván s konstantami zapsanými před jednotlivými větvemi. To sice není tak obecné řešení, jako je tomu v případě maker cond a condp, ovšem výsledkem by měl být rychlejší běh, protože se interně zkonstruuje mapa s jednotlivými větvemi a výběr větve je tak proveden v téměř konstantním čase a nikoli postupným porovnáváním. Navíc existuje speciální režim expanze pro celočíselné hodnoty.
Podívejme se nyní na jednotlivé způsoby použití makra case pro vytvoření rozhodovací konstrukce. Nejprve převod celočíselných hodnot na řetězce (což by ovšem bylo rozumnější implementovat přes mapy!):
(defn say-number [x] (case x 0 "nula" 1 "jedna" 2 "dva"))
Otestování funkce say-number:
(println (say-number 0)) nula (println (say-number 1)) jedna (println (say-number 2)) dva
Prozatím ovšem nemáme nijak zajištěnu funkčnost ve chvíli, kdy se předá hodnota neodpovídající žádné větvi:
(println (say-number 3)) Execution error (IllegalArgumentException) at user/say-number (REPL:3). No matching clause: 3
To lze ovšem velmi snadno napravit uvedením takzvané default větve, resp. default výrazu (bez konstanty):
(defn say-number [x] (case x 0 "nula" 1 "jedna" 2 "dva" "nezname cislo"))
Otestování nové podoby funkce say-number:
(println (say-number 0)) nula (println (say-number 1)) jedna (println (say-number 2)) dva (println (say-number 3)) nezname cislo
Nejsme ovšem v žádném případě omezeni pouze na celočíselné konstanty
(defn string->number [x] (case x "nula" 0 "jedna" 1 "dva" 2 -1))
Následuje otestování funkce string->number:
(println (string->number "nula")) 0 (println (string->number "jedna")) 1 (println (string->number "milion")) -1
8. Větší množství konstant u jednotlivých větví v makru case
Jednu větev je možné provést (resp. přesněji řečeno vyhodnotit) pro více konstant, které ovšem musí být v tomto případě umístěny do závorek:
(defn string->number [x] (case x ("nic" "nula") 0 "jedna" 1 ("dva" "dve") 2 -1))
Otestování předchozí funkce:
(println (string->number "nula")) 0 (println (string->number "dva")) 2 (println (string->number "dve")) 2
Konstantami mohou být v případě potřeby i vektory, mapy atd.
(defn say-vector [x] (case x [] "Empty vector" [1] "Vector with just one item" [1 2] "Sequence of two numbers 1 and 2" "I don't know"))
Vyzkoušení funkce say-vector:
(say-vector []) "Empty vector" (say-vector [1 2]) "Sequence of two numbers 1 and 2"
9. Kdy použít cond, condp a case?
Nyní již známe celou trojici standardních maker pro rozeskoky – cond, condp a case. Makro cond je nejobecnější, protože každá podmínka může být zadána samostatným výrazem. Ovšem za tuto univerzálnost platíme menší čitelností. Pokud je nutné testy implementovat podobnými výrazy, je výhodnější použít condp a v případě, že se porovnává výsledek nějakého výrazu s konstantami, doporučuje se použít case, viz též Clojure Style Guide.
Makro cond je sice nejuniverzálnější, ale taktéž delší na zápis a i výpočetně složité:
(cond (= x 0) "Nula" (= x 1) "Jedna" (= x 2) "Dva" :else "Nevim")
Makro condp umožňuje kratší zápis podmínky na jediném místě (první parametry makra):
(condp = x 0 "Nula" 1 "Jedna" 2 "Dva" "Nevim")
Nejméně univerzální je makro case, ovšem na druhou stranu je nejrychlejší:
(case x 0 "Nula" 1 "Jedna" 2 "Dva" "Nevim")
Rychlejší rozeskoky v případě použití makra case by mělo být možné i naměřit, pochopitelně ovšem za předpokladu, že důvěřujete mikrobenchmarkům (což je zrovna v případě JVM dosti problematické):
(defn f1 [x] (cond (= x 0) "Nula" (= x 1) "Jedna" (= x 2) "Dva" :else "Nevim")) (defn f2 [x] (condp = x 0 "Nula" 1 "Jedna" 2 "Dva" "Nevim")) (defn f3 [x] (case x 0 "Nula" 1 "Jedna" 2 "Dva" "Nevim")) (time (doseq [_ (range 100000000)] (doseq [x (range 0 4)] (f1 x)))) "Elapsed time: 4252.930607 msecs" (time (doseq [_ (range 100000000)] (doseq [x (range 0 4)] (f2 x)))) "Elapsed time: 4417.662463 msecs" (time (doseq [_ (range 100000000)] (doseq [x (range 0 4)] (f3 x)))) "Elapsed time: 3924.937731 msecs"
10. Expanze makra case a forma vygenerovaného bajtkódu
V krátkosti se ještě zmiňme o způsobu expanze makra case. Ta se v jednom ohledu odlišuje od expanze ostatních maker, a to konkrétně v tom směru, že výsledkem expanze není kód založený na speciální formě if. Namísto toho se v expanzi setkáme s case*, což je metoda implementovaná v Javě, která slouží ke konstrukci rozeskoku na úrovni bajtkódu (viz též zdrojové kódy samotného Clojure). Ostatně se o tom můžeme velmi snadno přesvědčit:
(macroexpand '(case x 0 "Nula" 1 "Jedna" 2 "Dva" "Nevim")) (let* [G__11190 x] (case* G__11190 0 0 "Nevim" {0 [0 "Nula"] 1 [1 "Jedna"] 2 [2 "Dva"]} :compact :int))
Abychom pochopili, jak celá technologie pracuje, musíme si připomenout, že Clojure pracuje tak, že každou zapsanou formu přeloží (po expanzi maker) do bajtkódu JVM a teprve poté je tato forma spuštěna. A právě pro překlad do bajtkódu se interně používá třída Compiler, která konkrétně interní symbol case* přeloží s využitím instrukce tableswitch určené pro implementaci konstrukce switch známé z Javy (resp. přesněji switch v případě, že jsou použita celá čísla nebo výčtový typ, nikoli řetězce). Touto problematikou jsme se již zabývali v článcích https://www.root.cz/clanky/pohled-pod-kapotu-jvm-9-cast-tajemstvi-instrukci-lookupswitch-a-tableswitch/#k04 a https://www.root.cz/clanky/pohled-pod-kapotu-jvm-9-cast-tajemstvi-instrukci-lookupswitch-a-tableswitch/#k07.
V praxi vypadá překlad funkce f3:
(defn f3 [x] (case x 0 "Nula" 1 "Jedna" 2 "Dva" "Nevim"))
následovně (zvýrazněna je instrukce tableswitch:
public static java.lang.Object invokeStatic(java.lang.Object x); 0 aload_0 [x] 1 aconst_null 2 astore_0 [x] 3 astore_1 [G__5991] 4 aload_1 [G__5991] 5 instanceof java.lang.Number [13] 8 ifeq 89 11 aload_1 [G__5991] 12 checkcast java.lang.Number [13] 15 invokevirtual java.lang.Number.intValue() : int [17] 18 tableswitch default: 89 case 0: 44 case 1: 59 case 2: 74 44 aload_1 [G__5991] 45 getstatic user$f3.const__0 : java.lang.Object [21] 48 invokestatic clojure.lang.Util.equiv(java.lang.Object, java.lang.Object) : boolean [27] 51 ifeq 89 54 ldc <String "Nula"> [29] 56 goto 91 59 aload_1 [G__5991] 60 getstatic user$f3.const__1 : java.lang.Object [32] 63 invokestatic clojure.lang.Util.equiv(java.lang.Object, java.lang.Object) : boolean [27] 66 ifeq 89 69 ldc <String "Jedna"> [34] 71 goto 91 74 aload_1 [G__5991] 75 getstatic user$f3.const__2 : java.lang.Object [37] 78 invokestatic clojure.lang.Util.equiv(java.lang.Object, java.lang.Object) : boolean [27] 81 ifeq 89 84 ldc <String "Dva"> [39] 86 goto 91 89 ldc <String "Nevim"> [41] 91 areturn
11. Makra ze jmenného prostoru clojure.core.logic
V této kapitole se ve stručnosti zmíníme o makrech, která jsou definována ve jmenném prostoru clojure.core.logic. V tomto jmenném prostoru nalezneme knihovnu, která do programovacího jazyka Clojure přináší konstrukce známé z Prologu, které byly popsány například v The Reasoned Schemer (viz též: The Reasoned Schemer, Second Edition: https://mitpress.mit.edu/books/reasoned-schemer-second-edition). Načtení funkcí, maker a dalších symbolů z tohoto jmenného prostoru si musíme explicitně vyžádat, například použitím require či use:
(use 'clojure.core.logic)
Mezi makra, která se zde používají pro vyhodnocení podmínek, patří condu, conda a conde. Jejich podrobnějším popisem se budeme zabývat v samostatném článku o logickém programování v Clojure.
12. Od „jednorozměrných“ rozhodovacích konstrukcí ke konstrukcím dvourozměrným
Existuje poměrně mnoho algoritmů postavených na takzvaných rozhodovacích tabulkách. Tyto tabulky mají podobu skutečných tabulek, v nichž se v hlavičkách sloupců i řádků zapisují podmínky a do buněk tabulky pak vypočtená hodnota popř. akce, která se má provést (tedy volání nějaké funkce). V běžných programovacích jazycích většinou nelze tyto rozhodovací tabulky zapisovat přímo, takže se setkáme buď s využitím map (popř. map obsahujících další mapy), vnořených konstrukcí switch apod. To ovšem není ani přehledné ani to neodpovídá implementovanému algoritmu (rozhodovací tabulka totiž může být součástí zadání). V programovacím jazyku Clojure však díky jeho makrosystému a homoikonicitě existuje minimálně jedno elegantní řešení, a to konkrétně ve formě makra nazvaného příznačně cond-table, jehož autorem je Daniel Gregoire.
Pokud je například nutné implementovat tabulku, která na základě dvou dvojic predikátů určí, která další funkce se má zavolat:
+------------++--------------+------------+ | || in-progress? | final-run? | +------------++--------------+------------+ | succeeded? || succeed | succeed | | failed? || retry | terminate | +------------++--------------+------------+
Je možné tuto rozhodovací tabulku zapsat do zdrojového kódu zcela přímočarým způsobem:
(cond-table :| in-progress? final-run? :| succeeded? (succeed) (succeed) :| failed? (retry) (terminate))
Taktéž někdy můžeme chtít pouze vrátit určitou hodnotu na základě podmínek a nevolat žádné funkce:
+------------++-----------------+----------------+--------------------+ | || operace=login | operace=logout | operace=index-page | +------------++-----------------+----------------+--------------------+ | status=200 || :index-page | :logout-page | :index-page | | status=401 || :not-authorized | :invalid-state | :invalid-state | | status=404 || :not-found | :not-found | :not-found) | +------------++-----------------+----------------+--------------------+
I tuto tabulku lze do zdrojového kódu zapsat přímočaře:
(cond-table :| (= oper :login) (= oper :logout) (= oper :index-page) :| (= status 200) :index-page :logout-page :index-page :| (= status 401) :not-authorized :invalid-state :invalid-state :| (= status 404) :not-found :not-found :not-found)
13. Makro cond-table
Makro cond-table sice není příliš rozsáhlé, ovšem je strukturováno velmi zajímavě. Nejdříve je (v expandovaném kódu) zkontrolována tabulka zapsaná programátorem a následně se z této tabulky sestaví sekvence forem cond (a ty jsou v další expanzi převedeny na speciální formu if). Úplný zdrojový kód tohoto makra vypadá následovně:
(defmacro cond-table "Produce a `cond` expression from a tabular representation of its clauses". [& items] (let [rows (validate-cond-table items) rights (first rows) ;; get right-hand conditions rights (if (and (symbol? (first rights)) (every? (partial = \_) (name (first rights)))) (next rights) rights) op-omitted? (= (count (second rows)) (inc (count rights))) [op rights] (if op-omitted? ['and rights] [(first rights) (next rights)])] (cons 'cond (mapcat (fn [[left-condition & exprs :as row]] (mapcat (fn [right-condition expr] ;; `cond` test/expr pair: (list (list op left-condition right-condition) expr)) rights exprs)) (next rows)))))
Při expanzi makra se ve výsledném kódu nejdříve provede kontrola (nebo řekněme validace) zapsané tabulky. To zajišťuje pomocná funkce nazvaná příznačně validate-cond-table:
(defn validate-cond-table "Validate the arguments passed to the `cond-table` macro and return the data of the table rows." [items] (let [rs (into [] (comp (partition-by (partial = :|)) (partition-all 2)) items) _ (when-not (every? #(= '(:|) (first %)) rs) (throw (IllegalArgumentException. "Each row in cond-table must begin with the keyword :|"))) rows (map second rs) ;; remove :| syntax header-count (count (first rows)) next-row-counts (into #{} (map count) (next rows)) next-rows-same-count? (= 1 (count next-row-counts)) ;; First row with blank first cell, for default `and` behavior default-header-validates? (= (inc header-count) (first next-row-counts)) ;; First row with custom op in first cell op-header-validates? (= header-count (first next-row-counts)) ;; All rows after the first must be same length and first row is either ;; the same length (because a custom op was supplied) or has one item ;; fewer (default of `and` is being leveraged). _ (when-not (and next-rows-same-count? (or default-header-validates? op-header-validates?)) (throw (IllegalArgumentException. "Every row after the first in cond-table must start with a predicate and include an expression for each cell in the table.")))] rows))
14. Příklady použití makra cond-table
Celá vyjadřovací síla nabízená makrem cond-table vynikne až ve chvíli, kdy toto makro použijeme v reálných příkladech. Například se můžeme rozhodnout na základě predikátů even?, odd?, zero? a not-zero? nad tím, jaká hodnota se má vrátit z funkce nazvané choose. Nejdříve ovšem musíme definovat predikát not-zero?, který není součástí standardní knihovny programovacího jazyka Clojure:
(defn not-zero? [n] (not (zero? n)))
nebo dáváte-li přednost threading makru:
(defn not-zero? [n] (-> n zero? not))
Funkce choose s rozhodovací tabulkou bude vypadat následovně:
(defn choose [x] (cond-table :| (even? x) (odd? x) :| (zero? x) :a :b :| (not-zero? x) :c :d))
Základní otestování funkčnosti:
(println (choose 0)) a (println (choose 1)) d (println (choose 2)) c (println (choose 3)) d (println (choose 4)) c
Praktičtější může být funkce použitá například v testu webové aplikace, která na základě aktuální operace a stavového kódu vráceného z webové aplikace rozhodne o tom, jaká stránka se má očekávat v dalším kroku. Jedná se tedy vlastně o implementaci stavového automatu založeného na vstupní hodnotě a na předchozím stavu:
(defn next-operation [oper status] (cond-table :| (= oper :login) (= oper :logout) (= oper :index-page) :| (= status 200) :index-page :logout-page :index-page :| (= status 401) :not-authorized :invalid-state :invalid-state :| (= status 404) :not-found :not-found :not-found))
Opět se podívejme, jak tato funkce pracuje, projdeme ovšem celý stavový prostor:
(doseq [operation [:login :logout :index-page]] (doseq [status [200 401 404]] (println operation status (next-operation operation status))))
S výsledkem:
:login 200 :index-page :login 401 :not-authorized :login 404 :not-found :logout 200 :logout-page :logout 401 :invalid-state :logout 404 :not-found :index-page 200 :index-page :index-page 401 :invalid-state :index-page 404 :not-found
15. Expanze makra cond-table
Makro cond-table se expanduje na formu obsahující další makro, konkrétně cond. Podmínky ze sloupců a řádků jsou spojeny s využitím logické spojky and, i když cond-table podporuje specifikaci libovolné jiné funkce (což se ale prakticky pravděpodobně nebude příliš často používat). Podívejme se nyní na konkrétní způsob expanze u tabulky s 2×2 podmínkami:
(macroexpand-1 '(cond-table :| (even? x) (odd? x) :| (zero? x) :a :b :| (not-zero? x) :c :d))
Výsledek expanze vypadá následovně – je jasně patrné spojení jednotlivých podmínek i to, že jsou vyčerpány všechny možné kombinace podmínek:
(cond (and (zero? x) (even? x)) :a (and (zero? x) (odd? x)) :b (and (not-zero? x) (even? x)) :c (and (not-zero? x) (odd? x)) :d)
Pochopitelně si expandované makro můžeme zobrazit i pro rozsáhlejší tabulku, zde konkrétně s 3×3 podmínkami, tedy s celkem devíti možnými kombinacemi:
(macroexpand-1 '(cond-table :| (= oper :login) (= oper :logout) (= oper :index-page) :| (= status 200) :index-page :logout-page :index-page :| (= status 401) :not-authorized :invalid-state :invalid-state :| (= status 404) :not-found :not-found :not-found))
Expandovaný kód vypadá následovně:
(cond (and (= status 200) (= oper :login)) :index-page (and (= status 200) (= oper :logout)) :logout-page (and (= status 200) (= oper :index-page)) :index-page (and (= status 401) (= oper :login)) :not-authorized (and (= status 401) (= oper :logout)) :invalid-state (and (= status 401) (= oper :index-page)) :invalid-state (and (= status 404) (= oper :login)) :not-found (and (= status 404) (= oper :logout)) :not-found (and (= status 404) (= oper :index-page)) :not-found)
16. A na konci je speciální forma if
Po úplné expanzi maker cond-table a cond musíme dojít k výrazu, v němž se bude vyskytovat speciální forma if, protože se jedná o jedinou podporovanou nízkoúrovňovou rozhodovací konstrukci programovacího jazyka Clojure (na rozdíl od Scheme, v němž jsou k dispozici i speciální formy and, or, when, unless a dokonce i cond – což jsou v Clojure „pouze“ makra). Je to ostatně patrné i při plné expanzi makra cond-table s 2×2 podmínkami:
(macroexpand-all '(cond-table :| (even? x) (odd? x) :| (zero? x) :a :b :| (not-zero? x) :c :d))
V expandovaném kódu nalezneme skutečně if, několik predikátů a interní symbol let*:
(if (let* [and__5514__auto__ (zero? x)] (if and__5514__auto__ (even? x) and__5514__auto__)) :a (if (let* [and__5514__auto__ (zero? x)] (if and__5514__auto__ (odd? x) and__5514__auto__)) :b (if (let* [and__5514__auto__ (not-zero? x)] (if and__5514__auto__ (even? x) and__5514__auto__)) :c (if (let* [and__5514__auto__ (not-zero? x)] (if and__5514__auto__ (odd? x) and__5514__auto__)) :d nil))))
Totéž platí pro rozhodovací tabulku s 3×3 podmínkami:
(macroexpand-all '(cond-table :| (= oper :login) (= oper :logout) (= oper :index-page) :| (= status 200) :index-page :logout-page :index-page :| (= status 401) :not-authorized :invalid-state :invalid-state :| (= status 404) :not-found :not-found :not-found))
S expanzí na kód:
(if (let* [and__5514__auto__ (= status 200)] (if and__5514__auto__ (= oper :login) and__5514__auto__)) :index-page (if (let* [and__5514__auto__ (= status 200)] (if and__5514__auto__ (= oper :logout) and__5514__auto__)) :logout-page (if (let* [and__5514__auto__ (= status 200)] (if and__5514__auto__ (= oper :index-page) and__5514__auto__)) :index-page (if (let* [and__5514__auto__ (= status 401)] (if and__5514__auto__ (= oper :login) and__5514__auto__)) :not-authorized (if (let* [and__5514__auto__ (= status 401)] (if and__5514__auto__ (= oper :logout) and__5514__auto__)) :invalid-state (if (let* [and__5514__auto__ (= status 401)] (if and__5514__auto__ (= oper :index-page) and__5514__auto__)) :invalid-state (if (let* [and__5514__auto__ (= status 404)] (if and__5514__auto__ (= oper :login) and__5514__auto__)) :not-found (if (let* [and__5514__auto__ (= status 404)] (if and__5514__auto__ (= oper :logout) and__5514__auto__)) :not-found (if (let* [and__5514__auto__ (= status 404)] (if and__5514__auto__ (= oper :index-page) and__5514__auto__)) :not-found nil)))))))))
17. Příloha: zdrojové kódy výše popsaných maker
V této příloze jsou vypsány podoby všech výše popsaných maker ze standardní knihovny programovacího jazyka Clojure. Tyto kódy jsou velmi dobrým studijním materiálem pro tvorbu vlastních maker:
(source if-not) (defmacro if-not "Evaluates test. If logical false, evaluates and returns then expr, otherwise else expr, if supplied, else nil." {:added "1.0"} ([test then] `(if-not ~test ~then nil)) ([test then else] `(if (not ~test) ~then ~else)))
(source if-let) (defmacro if-let "bindings => binding-form test If test is true, evaluates then with binding-form bound to the value of test, if not, yields else" {:added "1.0"} ([bindings then] `(if-let ~bindings ~then nil)) ([bindings then else & oldform] (assert-args (vector? bindings) "a vector for its binding" (nil? oldform) "1 or 2 forms after binding vector" (= 2 (count bindings)) "exactly 2 forms in binding vector") (let [form (bindings 0) tst (bindings 1)] `(let [temp# ~tst] (if temp# (let [~form temp#] ~then) ~else)))))
(source if-some) (defmacro if-some "bindings => binding-form test If test is not nil, evaluates then with binding-form bound to the value of test, if not, yields else" {:added "1.6"} ([bindings then] `(if-some ~bindings ~then nil)) ([bindings then else & oldform] (assert-args (vector? bindings) "a vector for its binding" (nil? oldform) "1 or 2 forms after binding vector" (= 2 (count bindings)) "exactly 2 forms in binding vector") (let [form (bindings 0) tst (bindings 1)] `(let [temp# ~tst] (if (nil? temp#) ~else (let [~form temp#] ~then))))))
(source and) (defmacro and "Evaluates exprs one at a time, from left to right. If a form returns logical false (nil or false), and returns that value and doesn't evaluate any of the other expressions, otherwise it returns the value of the last expr. (and) returns true." {:added "1.0"} ([] true) ([x] x) ([x & next] `(let [and# ~x] (if and# (and ~@next) and#))))
(source or) (defmacro or "Evaluates exprs one at a time, from left to right. If a form returns a logical true value, or returns that value and doesn't evaluate any of the other expressions, otherwise it returns the value of the last expression. (or) returns nil." {:added "1.0"} ([] nil) ([x] x) ([x & next] `(let [or# ~x] (if or# or# (or ~@next)))))
(source when) (defmacro when "Evaluates test. If logical true, evaluates body in an implicit do." {:added "1.0"} [test & body] (list 'if test (cons 'do body)))
(source when-not) (defmacro when-not "Evaluates test. If logical false, evaluates body in an implicit do." {:added "1.0"} [test & body] (list 'if test nil (cons 'do body)))
(source when-let) (defmacro when-let "bindings => binding-form test When test is true, evaluates body with binding-form bound to the value of test" {:added "1.0"} [bindings & body] (assert-args (vector? bindings) "a vector for its binding" (= 2 (count bindings)) "exactly 2 forms in binding vector") (let [form (bindings 0) tst (bindings 1)] `(let [temp# ~tst] (when temp# (let [~form temp#] ~@body)))))
(source when-some) (defmacro when-some "bindings => binding-form test When test is not nil, evaluates body with binding-form bound to the value of test" {:added "1.6"} [bindings & body] (assert-args (vector? bindings) "a vector for its binding" (= 2 (count bindings)) "exactly 2 forms in binding vector") (let [form (bindings 0) tst (bindings 1)] `(let [temp# ~tst] (if (nil? temp#) nil (let [~form temp#] ~@body)))))
(source when-first) (defmacro when-first "bindings => x xs Roughly the same as (when (seq xs) (let [x (first xs)] body)) but xs is evaluated only once" {:added "1.0"} [bindings & body] (assert-args (vector? bindings) "a vector for its binding" (= 2 (count bindings)) "exactly 2 forms in binding vector") (let [[x xs] bindings] `(when-let [xs# (seq ~xs)] (let [~x (first xs#)] ~@body))))
(source cond) (defmacro cond "Takes a set of test/expr pairs. It evaluates each test one at a time. If a test returns logical true, cond evaluates and returns the value of the corresponding expr and doesn't evaluate any of the other tests or exprs. (cond) returns nil." {:added "1.0"} [& clauses] (when clauses (list 'if (first clauses) (if (next clauses) (second clauses) (throw (IllegalArgumentException. "cond requires an even number of forms"))) (cons 'clojure.core/cond (next (next clauses))))))
(source condp) (defmacro condp "Takes a binary predicate, an expression, and a set of clauses. Each clause can take the form of either: Each clause can take the form of either: test-expr result-expr test-expr :>> result-fn Note :>> is an ordinary keyword. For each clause, (pred test-expr expr) is evaluated. If it returns logical true, the clause is a match. If a binary clause matches, the result-expr is returned, if a ternary clause matches, its result-fn, which must be a unary function, is called with the result of the predicate as its argument, the result of that call being the return value of condp. A single default expression can follow the clauses, and its value will be returned if no clause matches. If no default expression is provided and no clause matches, an IllegalArgumentException is thrown." {:added "1.0"} [pred expr & clauses] (let [gpred (gensym "pred__") gexpr (gensym "expr__") emit (fn emit [pred expr args] (let [[[a b c :as clause] more] (split-at (if (= :>> (second args)) 3 2) args) n (count clause)] (cond (= 0 n) `(throw (IllegalArgumentException. (str "No matching clause: " ~expr))) (= 1 n) a (= 2 n) `(if (~pred ~a ~expr) ~b ~(emit pred expr more)) :else `(if-let [p# (~pred ~a ~expr)] (~c p#) ~(emit pred expr more)))))] `(let [~gpred ~pred ~gexpr ~expr] ~(emit gpred gexpr clauses))))
(source conde) (defmacro conde "Logical disjunction of the clauses. The first goal in a clause is considered the head of that clause. Interleaves the execution of the clauses." [& clauses] (let [a (gensym "a")] `(fn [~a] (-inc (mplus* ~@(bind-conde-clauses a clauses))))))
(source condu) (defmacro condu "Committed choice. Once the head (first goal) of a clause has succeeded, remaining goals of the clause will only be run once. Non-relational." [& clauses] (let [a (gensym "a")] `(fn [~a] (ifu* ~@(map (cond-clauses a) clauses)))))
(source conda) (defmacro conda "Soft cut. Once the head of a clause has succeeded all other clauses will be ignored. Non-relational." [& clauses] (let [a (gensym "a")] `(fn [~a] (ifa* ~@(map (cond-clauses a) clauses)))))
(source cond->) (defmacro cond-> "Takes an expression and a set of test/form pairs. Threads expr (via ->) through each form for which the corresponding test expression is true. Note that, unlike cond branching, cond-> threading does not short circuit after the first true test expression." {:added "1.5"} [expr & clauses] (assert (even? (count clauses))) (let [g (gensym) steps (map (fn [[test step]] `(if ~test (-> ~g ~step) ~g)) (partition 2 clauses))] `(let [~g ~expr ~@(interleave (repeat g) (butlast steps))] ~(if (empty? steps) g (last steps)))))
(source cond->>) (defmacro cond->> "Takes an expression and a set of test/form pairs. Threads expr (via ->>) through each form for which the corresponding test expression is true. Note that, unlike cond branching, cond->> threading does not short circuit after the first true test expression." {:added "1.5"} [expr & clauses] (assert (even? (count clauses))) (let [g (gensym) steps (map (fn [[test step]] `(if ~test (->> ~g ~step) ~g)) (partition 2 clauses))] `(let [~g ~expr ~@(interleave (repeat g) (butlast steps))] ~(if (empty? steps) g (last steps)))))
(source case) (defmacro case "Takes an expression, and a set of clauses. Each clause can take the form of either: test-constant result-expr (test-constant1 ... test-constantN) result-expr The test-constants are not evaluated. They must be compile-time literals, and need not be quoted. If the expression is equal to a test-constant, the corresponding result-expr is returned. A single default expression can follow the clauses, and its value will be returned if no clause matches. If no default expression is provided and no clause matches, an IllegalArgumentException is thrown. Unlike cond and condp, case does a constant-time dispatch, the clauses are not considered sequentially. All manner of constant expressions are acceptable in case, including numbers, strings, symbols, keywords, and (Clojure) composites thereof. Note that since lists are used to group multiple constants that map to the same expression, a vector can be used to match a list if needed. The test-constants need not be all of the same type." {:added "1.2"} [e & clauses] (let [ge (with-meta (gensym) {:tag Object}) default (if (odd? (count clauses)) (last clauses) `(throw (IllegalArgumentException. (str "No matching clause: " ~ge))))] (if (> 2 (count clauses)) `(let [~ge ~e] ~default) (let [pairs (partition 2 clauses) assoc-test (fn assoc-test [m test expr] (if (contains? m test) (throw (IllegalArgumentException. (str "Duplicate case test constant: " test))) (assoc m test expr))) pairs (reduce1 (fn [m [test expr]] (if (seq? test) (reduce1 #(assoc-test %1 %2 expr) m test) (assoc-test m test expr))) {} pairs) tests (keys pairs) thens (vals pairs) mode (cond (every? #(and (integer? %) (<= Integer/MIN_VALUE % Integer/MAX_VALUE)) tests) :ints (every? keyword? tests) :identity :else :hashes)] (condp = mode :ints (let [[shift mask imap switch-type] (prep-ints tests thens)] `(let [~ge ~e] (case* ~ge ~shift ~mask ~default ~imap ~switch-type :int))) :hashes (let [[shift mask imap switch-type skip-check] (prep-hashes ge default tests thens)] `(let [~ge ~e] (case* ~ge ~shift ~mask ~default ~imap ~switch-type :hash-equiv ~skip-check))) :identity (let [[shift mask imap switch-type skip-check] (prep-hashes ge default tests thens)] `(let [~ge ~e] (case* ~ge ~shift ~mask ~default ~imap ~switch-type :hash-identity ~skip-check))))))))
18. Repositář s demonstračními příklady
Ukázkové části kódu s rozhodovacími konstrukcemi, které byly popsány v rámci předchozích kapitol, byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/clojure-examples/. Konkrétně se jedná o projekt conditions (https://github.com/tisnik/clojure-examples/tree/master/conditions), jehož zdrojový kód (jediný soubor) je možné postupně kopírovat do interaktivního prostředí představovaného smyčkou REPL. Příklad je pochopitelně možné spustit i v jeho plné podobě příkazem:
lein run
To ovšem do určité míry postrádá výukový charakter.
19. Odkazy na předchozí části seriálu o programovacím jazyku Clojure
- Clojure 1: Úvod
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm/ - Clojure 2: Symboly, kolekce atd.
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-2-cast/ - Clojure 3: Funkcionální programování
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-3-cast-funkcionalni-programovani/ - Clojure 4: Kolekce, sekvence a lazy sekvence
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-4-cast-kolekce-sekvence-a-lazy-sekvence/ - Clojure 5: Sekvence, lazy sekvence a paralelní programy
http://www.root.cz/clanky/clojure-a-bezpecne-aplikace-pro-jvm-sekvence-lazy-sekvence-a-paralelni-programy/ - Clojure 6: Podpora pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-6-futures-nejsou-jen-financni-derivaty/ - Clojure 7: Další funkce pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-7-dalsi-podpurne-prostredky-pro-paralelni-programovani/ - Clojure 8: Identity, stavy, neměnné hodnoty a reference
http://www.root.cz/clanky/programovaci-jazyk-clojure-8-identity-stavy-nemenne-hodnoty-a-referencni-typy/ - Clojure 9: Validátory, pozorovatelé a kooperace s Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-9-validatory-pozorovatele-a-kooperace-mezi-clojure-a-javou/ - Clojure 10: Kooperace mezi Clojure a Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-10-kooperace-mezi-clojure-a-javou-pokracovani/ - Clojure 11: Generátorová notace seznamu/list comprehension
http://www.root.cz/clanky/programovaci-jazyk-clojure-11-generatorova-notace-seznamu-list-comprehension/ - Clojure 12: Překlad programů z Clojure do bajtkódu JVM I:
http://www.root.cz/clanky/programovaci-jazyk-clojure-12-preklad-programu-z-clojure-do-bajtkodu-jvm/ - Clojure 13: Překlad programů z Clojure do bajtkódu JVM II:
http://www.root.cz/clanky/programovaci-jazyk-clojure-13-preklad-programu-z-clojure-do-bajtkodu-jvm-pokracovani/ - Clojure 14: Základy práce se systémem maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-14-zaklady-prace-se-systemem-maker/ - Clojure 15: Tvorba uživatelských maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-15-tvorba-uzivatelskych-maker/ - Programovací jazyk Clojure – triky při práci s řetězci
http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-retezci/ - Programovací jazyk Clojure – triky při práci s kolekcemi
http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-kolekcemi/ - Programovací jazyk Clojure – práce s mapami a množinami
http://www.root.cz/clanky/programovaci-jazyk-clojure-prace-s-mapami-a-mnozinami/ - Programovací jazyk Clojure – základy zpracování XML
http://www.root.cz/clanky/programovaci-jazyk-clojure-zaklady-zpracovani-xml/ - Programovací jazyk Clojure – testování s využitím knihovny Expectations
http://www.root.cz/clanky/programovaci-jazyk-clojure-testovani-s-vyuzitim-knihovny-expectations/ - Programovací jazyk Clojure – některé užitečné triky použitelné (nejenom) v testech
http://www.root.cz/clanky/programovaci-jazyk-clojure-nektere-uzitecne-triky-pouzitelne-nejenom-v-testech/ - Enlive – výkonný šablonovací systém pro jazyk Clojure
http://www.root.cz/clanky/enlive-vykonny-sablonovaci-system-pro-jazyk-clojure/ - Nástroj Leiningen a programovací jazyk Clojure: tvorba vlastních knihoven pro veřejný repositář Clojars
http://www.root.cz/clanky/nastroj-leiningen-a-programovaci-jazyk-clojure-tvorba-vlastnich-knihoven-pro-verejny-repositar-clojars/ - Novinky v Clojure verze 1.8.0
http://www.root.cz/clanky/novinky-v-clojure-verze-1–8–0/ - Asynchronní programování v Clojure s využitím knihovny core.async
http://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async/ - Asynchronní programování v Clojure s využitím knihovny core.async (pokračování)
http://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async-pokracovani/ - Asynchronní programování v Clojure s využitím knihovny core.async (dokončení)
http://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async-dokonceni/ - Vytváříme IRC bota v programovacím jazyce Clojure
http://www.root.cz/clanky/vytvarime-irc-bota-v-programovacim-jazyce-clojure/ - Gorilla REPL: interaktivní prostředí pro programovací jazyk Clojure
https://www.root.cz/clanky/gorilla-repl-interaktivni-prostredi-pro-programovaci-jazyk-clojure/ - Multimetody v Clojure aneb polymorfismus bez použití OOP
https://www.root.cz/clanky/multimetody-v-clojure-aneb-polymorfismus-bez-pouziti-oop/ - Práce s externími Java archivy v programovacím jazyku Clojure
https://www.root.cz/clanky/prace-s-externimi-java-archivy-v-programovacim-jazyku-clojure/ - Clojure 16: Složitější uživatelská makra
http://www.root.cz/clanky/programovaci-jazyk-clojure-16-slozitejsi-uzivatelska-makra/ - Clojure 17: Využití standardních maker v praxi
http://www.root.cz/clanky/programovaci-jazyk-clojure-17-vyuziti-standardnich-maker-v-praxi/ - Clojure 18: Základní techniky optimalizace aplikací
http://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/ - Clojure 19: Vývojová prostředí pro Clojure
http://www.root.cz/clanky/programovaci-jazyk-clojure-19-vyvojova-prostredi-pro-clojure/ - Clojure 20: Vývojová prostředí pro Clojure (Vimu s REPL)
http://www.root.cz/clanky/programovaci-jazyk-clojure-20-vyvojova-prostredi-pro-clojure-integrace-vimu-s-repl/ - Clojure 21: ClojureScript aneb překlad Clojure do JS
http://www.root.cz/clanky/programovaci-jazyk-clojure-21-clojurescript-aneb-preklad-clojure-do-javascriptu/ - Leiningen: nástroj pro správu projektů napsaných v Clojure
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (2)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-2/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (3)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-3/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (4)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-4/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (5)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-5/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (6)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-6/ - Programovací jazyk Clojure a databáze (1.část)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-databaze-1-cast/ - Pluginy pro Leiningen
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-pluginy-pro-leiningen/ - Programovací jazyk Clojure a knihovny pro práci s vektory a maticemi
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-knihovny-pro-praci-s-vektory-a-maticemi/ - Programovací jazyk Clojure a knihovny pro práci s vektory a maticemi (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-knihovny-pro-praci-s-vektory-a-maticemi-2/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk-2/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure/ - Seesaw: knihovna pro snadnou tvorbu GUI v azyce Clojure (2)
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-2/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure (3)
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-3/ - Programovací jazyk Clojure a práce s Gitem
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem/ - Programovací jazyk Clojure a práce s Gitem (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem-2/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk (dokončení)
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk-dokonceni/ - Pixie: lehký skriptovací jazyk s „kouzelnými“ schopnostmi
https://www.root.cz/clanky/pixie-lehky-skriptovaci-jazyk-s-kouzelnymi-schopnostmi/ - Programovací jazyk Pixie: funkce ze základní knihovny a použití FFI
https://www.root.cz/clanky/programovaci-jazyk-pixie-funkce-ze-zakladni-knihovny-a-pouziti-ffi/ - Novinky v Clojure verze 1.9.0
https://www.root.cz/clanky/novinky-v-clojure-verze-1–9–0/ - Validace dat s využitím knihovny spec v Clojure 1.9.0
https://www.root.cz/clanky/validace-dat-s-vyuzitim-knihovny-spec-v-clojure-1–9–0/ - Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure
https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure/ - Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure (2)
https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure-2/ - Incanter: prostředí pro statistické výpočty s grafickým výstupem založené na Clojure
https://www.root.cz/clanky/incanter-prostredi-pro-statisticke-vypocty-s-grafickym-vystupem-zalozene-na-clojure/ - Incanter: operace s maticemi
https://www.root.cz/clanky/incanter-operace-s-maticemi/ - Interpret programovacího jazyka Clojure integrovaný do Jupyter Notebooku
https://www.root.cz/clanky/interpret-programovaciho-jazyka-clojure-integrovany-do-jupyter-notebooku/ - Babashka: interpret Clojure určený pro rychlé spouštění utilit z příkazového řádku
https://www.root.cz/clanky/babashka-interpret-clojure-urceny-pro-rychle-spousteni-utilit-z-prikazoveho-radku/ - Pokročilý streaming založený na Apache Kafce, jazyku Clojure a knihovně Jackdaw
https://www.root.cz/clanky/pokrocily-streaming-zalozeny-na-apache-kafce-jazyku-clojure-a-knihovne-jackdaw/ - Pokročilý streaming založený na Apache Kafce, jazyku Clojure a knihovně Jackdaw (2. část)
https://www.root.cz/clanky/pokrocily-streaming-zalozeny-na-apache-kafce-jazyku-clojure-a-knihovne-jackdaw-2-cast/ - Pokročilý streaming založený na projektu Apache Kafka, jazyku Clojure a knihovně Jackdaw (streamy a kolony)
https://www.root.cz/clanky/pokrocily-streaming-zalozeny-na-projektu-apache-kafka-jazyku-clojure-a-knihovne-jackdaw-streamy-a-kolony/ - Řídicí struktury využitelné v programovacím jazyku Clojure
https://www.root.cz/clanky/ridici-struktury-vyuzitelne-v-programovacim-jazyku-clojure/
20. Odkazy na Internetu
- The Clojure Style Guide
https://github.com/bbatsov/clojure-style-guide - Rozhodovací tabulky a stromy
https://wikisofia.cz/wiki/Rozhodovac%C3%AD_tabulky_a_stromy - Special (Conditions)
https://github.com/clojureman/special - better-cond
https://cljdoc.org/d/better-cond/better-cond/2.1.0/doc/readme - A micro-library around the useful cond-let macro
https://cljdoc.org/d/com.walmartlabs/cond-let/1.0.0/doc/readme - An adaption of the Racket cond macro for Clojure
https://cljdoc.org/d/cond-plus/cond-plus/1.0.1/doc/readme - Makro cond-table
https://github.com/semperos/rankle/blob/master/src/com/semperos/rankle/util.clj - cond v jazyku Racket
https://docs.racket-lang.org/reference/if.html?q=cond#%28form._%28%28lib._racket%2Fprivate%2Fletstx-scheme..rkt%29._cond%29%29 - Learn Clojure – Flow Control
https://clojure.org/guides/learn/flow - clojure.core.logic
https://clojuredocs.org/clojure.core.logic - ETL Batch Processing With Kafka?
https://medium.com/swlh/etl-batch-processing-with-kafka-7f66f843e20d - ETL with Kafka
https://blog.codecentric.de/en/2018/03/etl-kafka/ - Building ETL Pipelines with Clojure and Transducers
https://www.grammarly.com/blog/engineering/building-etl-pipelines-with-clojure-and-transducers/ - pipeline (možné použít pro ETL)
https://clojuredocs.org/clojure.core.async/pipeline - On Track with Apache Kafka – Building a Streaming ETL Solution with Rail Data
https://www.confluent.io/blog/build-streaming-etl-solutions-with-kafka-and-rail-data/ - Kafka – Understanding Offset Commits
https://www.logicbig.com/tutorials/misc/kafka/committing-offsets.html - fundingcircle/jackdaw (na Clojars)
https://clojars.org/fundingcircle/jackdaw/versions/0.7.6 - Dokumentace ke knihovně jackdaw
https://cljdoc.org/d/fundingcircle/jackdaw/0.7.6/doc/readme - Jackdaw AdminClient API
https://cljdoc.org/d/fundingcircle/jackdaw/0.7.6/doc/jackdaw-adminclient-api - Jackdaw Client API
https://cljdoc.org/d/fundingcircle/jackdaw/0.7.6/doc/jackdaw-client-api - Kafka.clj
https://github.com/helins-io/kafka.clj - Použití nástroje Apache Kafka v aplikacích založených na mikroslužbách
https://www.root.cz/clanky/pouziti-nastroje-apache-kafka-v-aplikacich-zalozenych-na-mikrosluzbach/ - Apache Kafka: distribuovaná streamovací platforma
https://www.root.cz/clanky/apache-kafka-distribuovana-streamovaci-platforma/ - Real-Time Payments with Clojure and Apache Kafka (podcast)
https://www.evidentsystems.com/news/confluent-podcast-about-apache-kafka/ - Kafka and Clojure – Immutable event streams
https://practicalli.github.io/kafka-and-clojure/ - Kafka Streams, the Clojure way
https://blog.davemartin.me/posts/kafka-streams-the-clojure-way/ - dvlopt.kafka na GitHubu
https://github.com/helins-io/kafka.clj - kafka-streams-the-clojure-way na GitHubu
https://github.com/DaveWM/kafka-streams-the-clojure-way - babashka: A Clojure babushka for the grey areas of Bash
https://github.com/borkdude/babashka - Babashka and the Small Clojure Interpreter @ ClojureD 2020 (slajdy)
https://speakerdeck.com/borkdude/babashka-and-the-small-clojure-interpreter-at-clojured-2020 - Babashka: ukázky použití
https://github.com/borkdude/babashka/blob/master/doc/examples.md - clojureD 2020: „Babashka and Small Clojure Interpreter: Clojure in new contexts“ by Michiel Borkent
https://www.youtube.com/watch?v=Nw8aN-nrdEk&t=5s - Meetup #124 Babashka, implementing an nREPL server & game engines with Clojure
https://www.youtube.com/watch?v=0YmZYnwyHHc - The Last Programming Language (shrnutí vývoje programovacích jazyků)
https://www.youtube.com/watch?v=P2yr-3F6PQo - Shebang (Unix): Wikipedia EN
https://en.wikipedia.org/wiki/Shebang_(Unix) - Shebang (Unix): Wikipedia CZ
https://cs.wikipedia.org/wiki/Shebang_(Unix) - How to create Clojure notebooks in Jupyter
https://s01blog.wordpress.com/2017/12/10/how-to-create-clojure-notebooks-in-jupyter/ - Dokumentace k nástroji Conda
https://docs.conda.io/en/latest/ - Notebook interface
https://en.wikipedia.org/wiki/Notebook_interface - Jypyter: open source, interactive data science and scientific computing across over 40 programming languages
https://jupyter.org/ - Calysto Scheme
https://github.com/Calysto/calysto_scheme - scheme.py (základ projektu Calysto Scheme)
https://github.com/Calysto/calysto_scheme/blob/master/calysto_scheme/scheme.py - Humane test output for clojure.test
https://github.com/pjstadig/humane-test-output - iota
https://github.com/juxt/iota - 5 Differences between clojure.spec and Schema
https://lispcast.com/clojure.spec-vs-schema/ - Schema: Clojure(Script) library for declarative data description and validation
https://github.com/plumatic/schema - Zip archiv s Clojure 1.9.0
http://repo1.maven.org/maven2/org/clojure/clojure/1.9.0/clojure-1.9.0.zip - Clojure 1.9 is now available
https://clojure.org/news/2017/12/08/clojure19 - Deps and CLI Guide
https://clojure.org/guides/deps_and_cli - Changes to Clojure in Version 1.9
https://github.com/clojure/clojure/blob/master/changes.md - clojure.spec – Rationale and Overview
https://clojure.org/about/spec - Zip archiv s Clojure 1.8.0
http://repo1.maven.org/maven2/org/clojure/clojure/1.8.0/clojure-1.8.0.zip - Clojure 1.8 is now available
http://clojure.org/news/2016/01/19/clojure18 - Socket Server REPL
http://dev.clojure.org/display/design/Socket+Server+REPL - CLJ-1671: Clojure socket server
http://dev.clojure.org/jira/browse/CLJ-1671 - CLJ-1449: Add clojure.string functions for portability to ClojureScript
http://dev.clojure.org/jira/browse/CLJ-1449 - Launching a Socket Server
http://clojure.org/reference/repl_and_main#_launching_a_socket_server - API for clojure.string
http://clojure.github.io/clojure/branch-master/clojure.string-api.html - Clojars:
https://clojars.org/ - Seznam knihoven na Clojars:
https://clojars.org/projects - Clojure Cookbook: Templating HTML with Enlive
https://github.com/clojure-cookbook/clojure-cookbook/blob/master/07_webapps/7–11_enlive.asciidoc - An Introduction to Enlive
https://github.com/swannodette/enlive-tutorial/ - Enlive na GitHubu
https://github.com/cgrand/enlive - Expectations: příklady atd.
http://jayfields.com/expectations/ - Expectations na GitHubu
https://github.com/jaycfields/expectations - Lein-expectations na GitHubu
https://github.com/gar3thjon3s/lein-expectations - Testing Clojure With Expectations
https://semaphoreci.com/blog/2014/09/23/testing-clojure-with-expectations.html - Clojure testing TDD/BDD libraries: clojure.test vs Midje vs Expectations vs Speclj
https://www.reddit.com/r/Clojure/comments/1viilt/clojure_testing_tddbdd_libraries_clojuretest_vs/ - Testing: One assertion per test
http://blog.jayfields.com/2007/06/testing-one-assertion-per-test.html - Rewriting Your Test Suite in Clojure in 24 hours
http://blog.circleci.com/rewriting-your-test-suite-in-clojure-in-24-hours/ - Clojure doc: zipper
http://clojuredocs.org/clojure.zip/zipper - Clojure doc: parse
http://clojuredocs.org/clojure.xml/parse - Clojure doc: xml-zip
http://clojuredocs.org/clojure.zip/xml-zip - Clojure doc: xml-seq
http://clojuredocs.org/clojure.core/xml-seq - Parsing XML in Clojure
https://github.com/clojuredocs/guides - Clojure Zipper Over Nested Vector
https://vitalyper.wordpress.com/2010/11/23/clojure-zipper-over-nested-vector/ - Understanding Clojure's PersistentVector implementation
http://blog.higher-order.net/2009/02/01/understanding-clojures-persistentvector-implementation - Understanding Clojure's PersistentHashMap (deftwice…)
http://blog.higher-order.net/2009/09/08/understanding-clojures-persistenthashmap-deftwice.html - Assoc and Clojure's PersistentHashMap: part ii
http://blog.higher-order.net/2010/08/16/assoc-and-clojures-persistenthashmap-part-ii.html - Ideal Hashtrees (paper)
http://lampwww.epfl.ch/papers/idealhashtrees.pdf - 4Clojure
http://www.4clojure.com/ - ClojureDoc (rozcestník s dokumentací jazyka Clojure)
http://clojuredocs.org/ - Clojure (na Wikipedia EN)
http://en.wikipedia.org/wiki/Clojure - Clojure (na Wikipedia CS)
http://cs.wikipedia.org/wiki/Clojure - SICP (The Structure and Interpretation of Computer Programs)
http://mitpress.mit.edu/sicp/ - Pure function
http://en.wikipedia.org/wiki/Pure_function - Funkcionální programování
http://cs.wikipedia.org/wiki/Funkcionální_programování - Čistě funkcionální (datové struktury, jazyky, programování)
http://cs.wikipedia.org/wiki/Čistě_funkcionální - 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 - Clojure Macro Tutorial (Part II: The Compiler Strikes Back)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-ii-compiler.html - Clojure Macro Tutorial (Part III: Syntax Quote)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-ii-syntax.html - Tech behind Tech: Clojure Macros Simplified
http://techbehindtech.com/2010/09/28/clojure-macros-simplified/ - Fatvat – Exploring functional programming: Clojure Macros
http://www.fatvat.co.uk/2009/02/clojure-macros.html