Asynchronní programování v Clojure s využitím knihovny core.async (pokračování)

Pavel Tišnovský 9. 2. 2016

Před týdnem představená knihovna core.async nabízí i další možnosti využitelné při asynchronním programování. Jedná se o kanály s bufferem, podporu pro neblokující čtení a zápis dat do kanálů atd. Dnes se s těmito vlastnostmi seznámíme.

1. Asynchronní programování v Clojure s využitím knihovny core.async (pokračování)

Knihovna core.async, s níž jsme se seznámili v předchozí části seriálu o programovacím jazyku Clojure, nabízí programátorům kromě takzvaných kanálů a „go bloků“ i další funkce a makra, s nimiž se seznámíme dnes. Nejprve si řekneme více informací o funkci nazvané alts!, o níž jsme se již ve stručnosti zmínili minule. Připomeňme si, že této funkci se předá vektor kanálů a ve výchozím nastavení tato funkce náhodně vybere kanál, ze kterého bude číst. Bude se jednat o první kanál nabízející hodnotu, takže tato funkce „zaparkuje“ (zastaví) provádění go bloku jen tehdy, pokud jsou všechny kanály prázdné. Náhodný výběr neprázdného kanálu je zde důležitý, protože pokud by se kanály neustále vybíraly podle uvedeného pořadí, mohlo by docházet k efektu, který je znám pod termínem „vyhladovění“ (starvation) u těch kanálů, které se ve vektoru nachází na posledních pozicích.

Díky existenci funkce alts! je možné implementovat následující topologii propojení různých go bloků s využitím několika kanálů. Samozřejmě je možné v případě potřeby vytvořit i složitější topologii, k čemuž může dopomoci i funkce pipe:

+-----------+
| producent |           +------+
|     #1    |... >!.....|kanál1}.......
| go block  |           +------+      :
+-----------+                         :
                                      :
+-----------+                         :        +-----------+
| producent |           +------+      :        | konzument |
|     #2    |... >! ... |kanál2} ... alts! ... |           |
| go block  |           +------+      :        | go block  |
+-----------+                         :        +-----------+
                                      :
+-----------+                         :
| producent |           +------+      :
|     #3    |... >!.....|kanál3}......:
| go block  |           +------+
+-----------+

Poznámka: termínem „zaparkování“ se v kontextu knihovny clojure.async myslí stav, kdy je běh nějakého go bloku pozastaven a příslušné vlákno je uvolněno pro další použití. Jednou z předností této knihovny je totiž fakt, že se pro go bloky používá pouze určitý počet vláken, které se mezi bloky sdílí a dochází tak k efektivnějšímu využití vláken. Připomene nám to diagram z předchozí části ukazující rozdíl mezi klasickým multivláknovým přístupem a přístupem použitým v knihovně core.async:

Klasický přístup:

Vlákno 1........worker1........worker1........worker1........worker1
Vlákno 2........worker2........worker2........worker2........worker2
Vlákno 3........worker3........worker3........worker3........worker3
Vlákno 4........worker4........worker4........worker4........worker4

core.async:

Vlákno 1.worker1.worker4.worker1.worker3...worker1.worker3.worker2.worker4
Vlákno 2..worker2.worker3..worker1...worker4.worker2......worker1.worker3

Pojďme si nyní vyzkoušet, zda je tvrzení o náhodném výběru kanálu pravdivé. Napoví nám příklad se třemi producenty, z nichž každý ve svém go bloku zapisuje prvky do svého kanálu channel1, channel2 či channel3 (zápis se teoreticky nikdy nezastaví, protože prvky se generují nekonečnými lazy sekvencemi). Konzument je zde jeden – náhodně vybírá kanál, ze kterého se má prvek přečíst a počítá si statistiku, ze kterého kanálu bylo čtení provedeno. Konzumenta jsem napsal schválně poměrně rozvláčným způsobem, v praxi by ale šel naprogramovat lépe (například s použitím map, kde by klíči byly právě kanály):

; z kanalu se celkem precte 1000 hodnot
(go
    ; citace pouzite pro vyhodnoceni statistiky cteni
    (let [counter1 (atom 0)
          counter2 (atom 0)
          counter3 (atom 0)]
        ; precteme 1000 hodnot
        (dotimes [n 1000]
            ;(<! (timeout 1)) ; zkuste odkomentovat
            (let [[item channel] (alts! [channel1 channel2 channel3])]
                ; vyhodnoceni, ze ktereho kanalu se cteni provedlo
                (condp = channel
                    channel1 (swap! counter1 inc)
                    channel2 (swap! counter2 inc)
                    channel3 (swap! counter3 inc))))
        ; vypis ziskane statistiky
        (println "Channel #1 read " @counter1 "times")
        (println "Channel #2 read " @counter2 "times")
        (println "Channel #3 read " @counter3 "times")))

Úplný zdrojový kód celého příkladu pak vypadá následovně:

(ns async6.core
    (:gen-class))
 
; nacteme vsechny potrebne funkce, makra a symboly z knihovny
; (schvalne se nenacitaji vsechny funkce, protoze by jejich jmena
;  prepsala takove zakladni funkce, jako je map apod.)
(require '[clojure.core.async :refer (go chan >! <! timeout alts!)])
 
(defn wait
    "Pozastaveni hlavniho vlakna - simulace interaktivni prace."
    []
    (Thread/sleep 5000))
 
(defn -main
    "Tato funkce se spusti automaticky nastrojem Leiningen."
    [& args]
    (println "Start")
    ; vytvorime kanaly
    (let [channel1 (chan)
          channel2 (chan)
          channel3 (chan)]
 
        ; kontinualni posilani zprav do trech kanalu v trojici asynchronnich bloku
        (go
            (doseq [i (range)]
                (>! channel1 i)))
        (go
            (doseq [i (range)]
                (>! channel2 i)))
        (go
            (doseq [i (range)]
                (>! channel3 i)))
 
        (println "producers started")
 
        ; z kanalu se celkem precte 1000 hodnot
        (go
            ; citace pouzite pro vyhodnoceni statistiky cteni
            (let [counter1 (atom 0)
                  counter2 (atom 0)
                  counter3 (atom 0)]
                ; precteme 1000 hodnot
                (dotimes [n 1000]
                    ;(<! (timeout 1)) ; zkuste odkomentovat
                    (let [[item channel] (alts! [channel1 channel2 channel3])]
                        ; vyhodnoceni, ze ktereho kanalu se cteni provedlo
                        (condp = channel
                            channel1 (swap! counter1 inc)
                            channel2 (swap! counter2 inc)
                            channel3 (swap! counter3 inc))))
                ; vypis ziskane statistiky
                (println "Channel #1 read " @counter1 "times")
                (println "Channel #2 read " @counter2 "times")
                (println "Channel #3 read " @counter3 "times")))
 
    ; chvili pockame
    (wait)
    (println "Finish")
    (System/exit 0)))

Pokud vás zajímá statistika pro tisíc prvků, podívejme se na výstup ze tří běhů:

Start
producers started
Channel #1 read  300 times
Channel #2 read  350 times
Channel #3 read  350 times
Finish
Start
producers started
Channel #1 read  341 times
Channel #2 read  331 times
Channel #3 read  328 times
Finish
Start
producers started
Channel #1 read  322 times
Channel #2 read  327 times
Channel #3 read  351 times
Finish

Z výsledků je patrné, že se skutečně kanály vybíraly náhodně, protože počet výběrů každého kanálu se blíží k hodnotě 1000/3.

2. Určení priorit při použití funkce alts!

Ve skutečnosti jsou však možnosti funkce alts! ještě větší. Pokud se přidá nepovinný parametr :priority s hodnotou nastavenou na true (či na libovolnou hodnotu, která není nepravdivá), bude se kanál pro čtení vybírat v tom pořadí, v jakém je zadán ve vektoru. Pokud tedy bude první kanál neustále plný, bude se využívat pouze ten a u ostatních kanálů dojde k jevu známému pod termínem „starvation“ (hladovění). Podívejme se na druhou verzi konzumenta:

; z kanalu se celkem precte 1000 hodnot
(go
    ; citace pouzite pro vyhodnoceni statistiky cteni
    (let [counter1 (atom 0)
          counter2 (atom 0)
          counter3 (atom 0)]
        ; precteme 1000 hodnot
        (dotimes [n 1000]
            ;(<! (timeout 1)) ; zkuste odkomentovat
            ; --------------------------------------------------------------------------
            (let [[item channel] (alts! [channel1 channel2 channel3] :priority true)]
            ; --------------------------------------------------------------------------
                ; vyhodnoceni, ze ktereho kanalu se cteni provedlo
                (condp = channel
                    channel1 (swap! counter1 inc)
                    channel2 (swap! counter2 inc)
                    channel3 (swap! counter3 inc))))
        ; vypis ziskane statistiky
        (println "Channel #1 read " @counter1 "times")
        (println "Channel #2 read " @counter2 "times")
        (println "Channel #3 read " @counter3 "times")))

Výsledky běhu pro takto změněného konzumenta ukazují, že první kanál je skutečně využívám mnohem častěji, než kanál druhý a ten je využíván častěji, než kanál třetí (vzhledem k tomu, že je konzument velmi rychlý, nemusí být v prvním kanále žádný prvek a proto také se v některých případech provádí čtení ze druhého a přinejhorším i ze třetího kanálu):

Start
producers started
Channel #1 read  647 times
Channel #2 read  243 times
Channel #3 read  110 times
Finish
Start
producers started
Channel #1 read  623 times
Channel #2 read  299 times
Channel #3 read  78 times
Finish
Start
producers started
Channel #1 read  728 times
Channel #2 read  217 times
Channel #3 read  55 times
Finish

Můžeme si položit otázku, co se stane ve chvíli, kdy je producent používající první kanál rychlejší než konzument. Bude se vůbec provádět čtení z dalších kanálů? To lze snadno zajistit zpomalením konzumenta (odkomentováním vyznačeného řádku kódu):

; z kanalu se celkem precte 1000 hodnot
(go
    ; citace pouzite pro vyhodnoceni statistiky cteni
    (let [counter1 (atom 0)
          counter2 (atom 0)
          counter3 (atom 0)]
        ; precteme 1000 hodnot
        (dotimes [n 1000]
            (<! (timeout 1)) ; pozdrzeni konzumenta
            ; --------------------------------------------------------------------------
            (let [[item channel] (alts! [channel1 channel2 channel3] :priority true)]
            ; --------------------------------------------------------------------------
                ; vyhodnoceni, ze ktereho kanalu se cteni provedlo
                (condp = channel
                    channel1 (swap! counter1 inc)
                    channel2 (swap! counter2 inc)
                    channel3 (swap! counter3 inc))))
        ; vypis ziskane statistiky
        (println "Channel #1 read " @counter1 "times")
        (println "Channel #2 read " @counter2 "times")
        (println "Channel #3 read " @counter3 "times")))

Výsledek mluví za vše, proto ho nemá cenu široce komentovat (aneb – starvation v praxi):

Start
producers started
Channel #1 read  1000 times
Channel #2 read  0 times
Channel #3 read  0 times
Finish

3. První demonstrační příklad: použití funkce alts! s určením priorit

Následuje výpis úplného zdrojového kódu dnešního prvního demonstračního příkladu (celý projekt naleznete na GitHubu, konkrétně na adrese https://github.com/tisnik/clojure-examples/tree/master/async6):

(ns async6.core
    (:gen-class))
 
; nacteme vsechny potrebne funkce, makra a symboly z knihovny
; (schvalne se nenacitaji vsechny funkce, protoze by jejich jmena
;  prepsala takove zakladni funkce, jako je map apod.)
(require '[clojure.core.async :refer (go chan >! <! timeout alts!)])
 
(defn wait
    "Pozastaveni hlavniho vlakna - simulace interaktivni prace."
    []
    (Thread/sleep 5000))
 
(defn -main
    "Tato funkce se spusti automaticky nastrojem Leiningen."
    [& args]
    (println "Start")
    ; vytvorime kanaly
    (let [channel1 (chan)
          channel2 (chan)
          channel3 (chan)]
 
        ; kontinualni posilani zprav do trech kanalu v trojici asynchronnich bloku
        (go
            (doseq [i (range)]
                (>! channel1 i)))
        (go
            (doseq [i (range)]
                (>! channel2 i)))
        (go
            (doseq [i (range)]
                (>! channel3 i)))
 
        (println "producers started")
 
        ; z kanalu se celkem precte 1000 hodnot
        (go
            ; citace pouzite pro vyhodnoceni statistiky cteni
            (let [counter1 (atom 0)
                  counter2 (atom 0)
                  counter3 (atom 0)]
                ; precteme 1000 hodnot
                (dotimes [n 1000]
                    ;(<! (timeout 1)) ; zkuste odkomentovat
                    (let [[item channel] (alts! [channel1 channel2 channel3] :priority true)]
                        ; vyhodnoceni, ze ktereho kanalu se cteni provedlo
                        (condp = channel
                            channel1 (swap! counter1 inc)
                            channel2 (swap! counter2 inc)
                            channel3 (swap! counter3 inc))))
                ; vypis ziskane statistiky
                (println "Channel #1 read " @counter1 "times")
                (println "Channel #2 read " @counter2 "times")
                (println "Channel #3 read " @counter3 "times")))
 
    ; chvili pockame
    (wait)
    (println "Finish")
    (System/exit 0)))

Projektový soubor:

(defproject async6 "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.7.0"]
                 [org.clojure/core.async "0.2.374"]]
  :main ^:skip-aot async6.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

4. Vytvoření kanálu s vlastnostmi bufferu (fronty)

Kanály, které jsme až doposud pro komunikaci mezi go bloky používali, mohly obsahovat pouze jediný prvek, takže se jejich chování dá shrnout takto:

  • Zápis do prázdného kanálu je neblokující operace (kanál se zápisem zaplní, další zápis již tedy bude blokující, pokud nedojde ke čtení).
  • Zápis do neprázdného kanálu je blokující operace.
  • Čtení z prázdného kanálu je blokující operace (počká se na další zápis).
  • Čtení z neprázdného kanálu je neblokující operace (kanál se vyprázdní).

Docházíme tedy k již známému schématu:

+-----------+                              +-----------+
| producent |           +-----+            | konzument |
|           |... >! ... |kanál} ... <! ... |           |
| go block  |           +-----+            | go block  |
+-----------+                              +-----------+

Kanál však může mít i větší kapacitu – potom vlastně pracuje jako buffer či fronta, což znamená, že i větší počet zápisů nemusí být nutně blokující operace:

+-----------+                                     +-----------+
| producent |           +------------+            | konzument |
|           |... >! ... |kanál+fronta} ... <! ... |           |
| go block  |           +------------+            | go block  |
+-----------+                                     +-----------+

Jak se takový kanál vytvoří? Ve skutečnosti velmi snadno, protože stačí jeho kapacitu uvést už při konstrukci kanálu nepovinným parametrem předaným do funkce chan:

(let [channelX (chan 42)]
    ...
    ...
    ...
)

5. Druhý demonstrační příklad: práce s kanálem s nenulovou kapacitou

V dalším demonstračním příkladu budeme stále používat tři producenty, podobně jako v příkladu prvním, ovšem jeden producent bude zapisovat do kanálu s větší kapacitou, zatímco ostatní dva producenti se budou muset spokojit s normálním kanálem bez bufferu:

; vytvorime kanaly
(let [channel1 (chan)
      channel2 (chan 100) ; tento kanal ma prirazen buffer o velikosti 100 prvku
      channel3 (chan)]
      ...
      ...
      ...
)

Co to bude znamenat v praxi? Velmi pravděpodobně dojde k tomu, že právě tento kanál bude při čtení konzumentem v daný okamžik neprázdný a bude tak vybírán častěji. Můžeme se o tom přesvědčit (konzument nepoužívá priority):

Start
producers started
Channel #1 read  206 times
Channel #2 read  573 times
Channel #3 read  221 times
Finish
Start
producers started
Channel #1 read  140 times
Channel #2 read  728 times
Channel #3 read  132 times
Finish
Start
producers started
Channel #1 read  191 times
Channel #2 read  599 times
Channel #3 read  210 times
Finish

Skutečně – ze druhého kanálu je čtení prováděno výrazně častěji, než z prvního či třetího kanálu.

Podívejme se nyní na úplný zdrojový kód tohoto demonstračního příkladu:

(ns async7.core
    (:gen-class))
 
; nacteme vsechny potrebne funkce, makra a symboly z knihovny
; (schvalne se nenacitaji vsechny funkce, protoze by jejich jmena
;  prepsala takove zakladni funkce, jako je map apod.)
(require '[clojure.core.async :refer (go chan >! <! timeout alts!)])
 
(defn wait
    "Pozastaveni hlavniho vlakna - simulace interaktivni prace."
    []
    (Thread/sleep 5000))
 
(defn -main
    "Tato funkce se spusti automaticky nastrojem Leiningen."
    [& args]
    (println "Start")
    ; vytvorime kanaly
    (let [channel1 (chan)
          channel2 (chan 100) ; tento kanal ma prirazen buffer o velikosti 100 prvku
          channel3 (chan)]
 
        ; kontinualni posilani zprav do trech kanalu v trojici asynchronnich bloku
        (go
            (doseq [i (range)]
                (>! channel1 i)))
        (go
            (doseq [i (range)]
                (>! channel2 i)))
        (go
            (doseq [i (range)]
                (>! channel3 i)))
 
        (println "producers started")
 
        ; z kanalu se celkem precte 1000 hodnot
        (go
            ; citace pouzite pro vyhodnoceni statistiky cteni
            (let [counter1 (atom 0)
                  counter2 (atom 0)
                  counter3 (atom 0)]
                ; precteme 1000 hodnot
                (dotimes [n 1000]
                    ;(<! (timeout 1)) ; zkuste odkomentovat
                    (let [[item channel] (alts! [channel1 channel2 channel3])]
                        ; vyhodnoceni, ze ktereho kanalu se cteni provedlo
                        (condp = channel
                            channel1 (swap! counter1 inc)
                            ; s velkou pravdepodobnosti se prvek precte z tohoto kanalu
                            ; (protoze ma data 'predpripravena')
                            channel2 (swap! counter2 inc)
                            channel3 (swap! counter3 inc))))
                ; vypis ziskane statistiky
                (println "Channel #1 read " @counter1 "times")
                (println "Channel #2 read " @counter2 "times")
                (println "Channel #3 read " @counter3 "times")))
 
    ; chvili pockame
    (wait)
    (println "Finish")
    (System/exit 0)))

Projektový soubor:

(defproject async7 "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.7.0"]
                 [org.clojure/core.async "0.2.374"]]
  :main ^:skip-aot async7.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

6. Kanál s „dropping bufferem“

V některých případech může být výhodné používat kanál s nastavenou (nenulovou) kapacitou, ovšem s modifikovaným chováním – ve chvíli, kdy je již buffer kanálu zaplněn, bude další zápis do něj ignorován (a bude tedy za všech okolností neblokující, ovšem na úkor ztráty dat). Příkladem může být kanál naplňovaný událostmi z GUI – většinou nemá smysl se snažit provést naprosto všechny operace, které uživatel zadal (například nervózním poklepáváním na tlačítko OK). V tomto případě pomůže použití kanálu s bufferem typu dropping buffer. Název tohoto bufferu naznačuje jeho funkci: pokud je kanál plný, další zápisy do něj budou ignorovány, o čemž se producent samozřejmě dozví (přes návratovou hodnotu funkce typu „put“).

+-----------+                                     +-----------+
| producent |           +------------+            | konzument |
|           |... >!.....|kanál+fronta} ... <! ... |           |
| go block  |        :  +------------+            | go block  |
+-----------+        :                            +-----------+
                     :
                     :
                     v /dev/null

Kanál s takovýmto chováním se nastavuje následujícím způsobem:

; vytvorime kanal s dropping bufferem o zadane kapacite
(let [channel (chan (dropping-buffer 10))]
      ...
      ...
      ...
)

Poznámka: povšimněte si, že funkce dropping-buffer je parametrem funkce chan. Je častou chybou se pokoušet vytvořit kanál takto:

; ŠPATNĚ!!! vytvorime kanal s dropping bufferem o zadane kapacite
(let [channel (dropping-buffer 10)]
      ...
      ...
      ...
)

Poznámka 2: vyzkoušejte si ve smyčce REPL rozdíl mezi posledními dvěma příkazy (druhý příkaz vytváří buffer použitý v běžné frontě):

(require '[clojure.core.async :refer (buffer dropping-buffer unblocking-buffer?)])
 
(unblocking-buffer? (buffer 10))
 
(unblocking-buffer? (dropping-buffer 10))

7. Třetí demonstrační příklad: práce s kanálem s „dropping bufferem“

Předchozí demonstrační příklad nyní přepíšeme tak, že se v něm bude nacházet jediný producent a jediný konzument. Pro komunikaci mezi producentem a konzumentem se bude používat kanál s přidruženým dropping bufferem. Producent vypadá takto:

; poslani zprav do kanalu, celkem 1000 hodnot od 0 do 999
(go
    (doseq [i (range 0 1000)]
        (>! channel i))
    (close! channel)))

Konzument je implementován takovým způsobem, že si ve smyčce vytváří vektor s přečtenými daty (smyčka je představována dvojicí loop-recur):

; cteni zprav z kanalu (to zahajime drive, at je jistota
; ze zapisy neskonci moc brzo)
(go
    (loop [result []]
        (<! (timeout 1))
        (let [item (<! channel)] ; pokud je kanal zavreny, vrati se nil
            (if item             ; v pripade, ze se prvek precetl
               (recur (conj result item)) ; prida se do kolekce
               (println result)))))       ; jinak koncime

Podívejme se na výstup tohoto příkladu, na němž se přesně ukazuje chování dropping bufferu ve chvíli, kdy producent generuje větší množství dat, než může konzument zpracovat. Prvních deset položek (jde o kapacitu bufferu) se zapíše a zpracuje, potom však konzument nebude dostatečně rychle číst data a tudíž dojde k jejich ztrátě („drop“). Výsledek může vypadat takto (jedenáctou položku konzument ještě zpracoval, dvanáctou už nestihl, dále je situace jen horší):

Start
consumer started
producer started
[0 1 2 3 4 5 6 7 8 9 10 15 32 67 102 129 139 200 260 318 378 448 510 576 658 749 839 852 943]
Finish
Start
consumer started
producer started
[0 1 2 3 4 5 6 7 8 9 10 25 126 207 305 386 516 607 718 857 993]
Finish
Start
consumer started
producer started
[0 1 2 3 4 5 6 7 8 9 10 39 84 118 136 217 274 352 407 484 536 603 667 742 821 912]
Finish

Úplný zdrojový kód tohoto – v pořadí již třetího – demonstračního příkladu vypadá následovně:

(ns async8.core
    (:gen-class))
 
; nacteme vsechny potrebne funkce, makra a symboly z knihovny
; (schvalne se nenacitaji vsechny funkce, protoze by jejich jmena
;  prepsala takove zakladni funkce, jako je map apod.)
(require '[clojure.core.async :refer (go chan >! <! timeout dropping-buffer close!)])
 
(defn wait
    "Pozastaveni hlavniho vlakna - simulace interaktivni prace."
    []
    (Thread/sleep 5000))
 
(defn -main
    "Tato funkce se spusti automaticky nastrojem Leiningen."
    [& args]
    (println "Start")
    ; vytvorime kanal s dropping bufferem o zadane kapacite
    (let [channel (chan (dropping-buffer 10))]
 
        ; cteni zprav z kanalu (to zahajime drive, at je jistota
        ; ze zapisy neskonci moc brzo)
        (go
            (loop [result []]
                (<! (timeout 1))
                (let [item (<! channel)] ; pokud je kanal zavreny, vrati se nil
                    (if item             ; v pripade, ze se prvek precetl
                       (recur (conj result item)) ; prida se do kolekce
                       (println result)))))       ; jinak koncime
 
        (println "consumer started")
 
        ; poslani zprav do kanalu, celkem 1000 hodnot od 0 do 999
        (go
            (doseq [i (range 0 1000)]
                (>! channel i))
            (close! channel)))
 
        (println "producer started")
 
    ; chvili pockame
    (wait)
    (println "Finish")
    (System/exit 0))

Projektový soubor:

(defproject async8 "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.7.0"]
                 [org.clojure/core.async "0.2.374"]]
  :main ^:skip-aot async8.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

8. Kanál se „sliding bufferem“

Chování dropping bufferu popsaného v předchozích dvou kapitolách je charakterizováno tím, že v případě zaplněného kanálu dochází na straně producenta (tj. kódu, který do kanálu zapisuje) ke ztrátě dat – nová data prostě budou ignorována. V knihovně clojure.core však ještě existuje jeden typ kanálu, jehož chování je vlastně zcela opačné, i když se opět jedná o jednu z možných variant fronty. Tento alternativní typ kanálu používá takzvaný „sliding buffer“, který se vyznačuje tím, že v případě zaplnění kanálu dochází při dalším zápisu k postupnému přepisu již uložených prvků, přičemž se z kanálu vymaže nejdříve zapsaný prvek (prvky tedy z bufferu „přepadávají“). Skutečně se tedy jedná o jednu z variant fronty, ovšem s tím, že z fronty vedou dva výstupy: jeden je čtený konzumentem a druhý vede (eufemicky řečeno) do /dev/null:

+-----------+                                     +-----------+
| producent |           +------------+            | konzument |
|           |... >! ... |kanál+fronta} ... <! ... |           |
| go block  |           +------------+...         | go block  |
+-----------+                           :         +-----------+
                                        :
                                        :
                                        v /dev/null

Kanál s těmito charakteristikami se vytvoří následovně:

; vytvorime kanal se sliding bufferem o zadane kapacite
(let [channel (chan (sliding-buffer 10))]
      ...
      ...
      ...
)

Poznámka: povšimněte si, že funkce sliding-buffer je parametrem funkce chan. Je častou chybou se pokoušet vytvořit kanál takto:

; ŠPATNĚ!!! vytvorime kanal se sliding bufferem o zadane kapacite
(let [channel (sliding-buffer 10)]
      ...
      ...
      ...
)

Poznámka 2: vyzkoušejte si ve smyčce REPL rozdíl mezi posledními dvěma příkazy (druhý příkaz vytváří buffer použitý v běžné frontě):

(require '[clojure.core.async :refer (buffer dropping-buffer sliding-buffer?)])
 
(unblocking-buffer? (buffer 10))
 
(unblocking-buffer? (sliding-buffer 10))

9. Čtvrtý demonstrační příklad: práce s kanálem se „sliding bufferem“

Opět provedeme úpravu předchozího demonstračního příkladu, a to přepisem typu bufferu z dropping bufferu na sliding buffer:

; vytvorime kanal s sliding bufferem o zadane kapacite
(let [channel (chan (sliding-buffer 10))]

Úprava je sice jednoduchá, ovšem chování producenta a konzumenta se zcela změní, což si ihned ověříme (konzument je opět výrazně pomalejší než producent, takže nutně dojde ke ztrátě dat, protože zápis je nyní neblokující operace):

Start
consumer started
producer started
[0 5 33 73 117 167 212 256 305 350 409 477 537 561 622 699 786 870 951 990 991 992 993 994 995 996 997 998 999]
Finish
Start
consumer started
producer started
[0 9 16 39 63 103 145 187 223 268 315 363 415 476 529 602 674 750 818 898 976 989 990 991 992 993 994 995 996 997 998 999]
Finish
Start
consumer started
producer started
[0 8 41 117 164 206 252 341 400 480 607 674 730 893 983 990 991 992 993 994 995 996 997 998 999]
Finish

Povšimněte si, že posledních deset hodnot se přečetlo korektně a ve správném pořadí, a to právě z toho důvodu, že se jedná o poslední hodnoty zapsané do kanálu.

Úplný zdrojový kód dnešního v pořadí již třetího demonstračního příkladu vypadá následovně:

(ns async9.core
    (:gen-class))
 
; nacteme vsechny potrebne funkce, makra a symboly z knihovny
; (schvalne se nenacitaji vsechny funkce, protoze by jejich jmena
;  prepsala takove zakladni funkce, jako je map apod.)
(require '[clojure.core.async :refer (go chan >! <! timeout sliding-buffer close!)])
 
(defn wait
    "Pozastaveni hlavniho vlakna - simulace interaktivni prace."
    []
    (Thread/sleep 5000))
 
(defn -main
    "Tato funkce se spusti automaticky nastrojem Leiningen."
    [& args]
    (println "Start")
    ; vytvorime kanal s sliding bufferem o zadane kapacite
    (let [channel (chan (sliding-buffer 10))]
 
        ; cteni zprav z kanalu (to zahajime drive, at je jistota
        ; ze zapisy neskonci moc brzo)
        (go
            (loop [result []]
                (<! (timeout 1))
                (let [item (<! channel)] ; pokud je kanal zavreny, vrati se nil
                    (if item             ; v pripade, ze se prvek precetl
                       (recur (conj result item)) ; prida se do kolekce
                       (println result)))))       ; jinak koncime
 
        (println "consumer started")
 
        ; poslani zprav do kanalu, celkem 1000 hodnot od 0 do 999
        (go
            (doseq [i (range 0 1000)]
                (>! channel i))
            (close! channel)))
 
        (println "producer started")
 
    ; chvili pockame
    (wait)
    (println "Finish")
    (System/exit 0))

Projektový soubor:

(defproject async9 "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.7.0"]
                 [org.clojure/core.async "0.2.374"]]
  :main ^:skip-aot async9.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

10. Repositář s demonstračními příklady

Všechny čtyři demonstrační příklady, které jsme si v dnešním článku popsali, byly uloženo do Git repositáře dostupného na adrese https://github.com/tisnik/clojure-examples. V tabulce zobrazené pod tímto odstavcem naleznete na zdrojové kódy demonstračních příkladů přímé odkazy:

11. Odkazy na předchozí části tohoto seriálu

  1. Clojure 1: Úvod
    http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm/
  2. Clojure 2: Symboly, kolekce atd.
    http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-2-cast/
  3. 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/
  4. 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/
  5. 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/
  6. Clojure 6: Podpora pro paralelní programování
    http://www.root.cz/clanky/programovaci-jazyk-clojure-6-futures-nejsou-jen-financni-derivaty/
  7. Clojure 7: Další funkce pro paralelní programování
    http://www.root.cz/clanky/programovaci-jazyk-clojure-7-dalsi-podpurne-prostredky-pro-paralelni-programovani/
  8. 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/
  9. 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/
  10. Clojure 10: Kooperace mezi Clojure a Javou
    http://www.root.cz/clanky/programovaci-jazyk-clojure-10-kooperace-mezi-clojure-a-javou-pokracovani/
  11. Clojure 11: Generátorová notace seznamu/list comprehension
    http://www.root.cz/clanky/programovaci-jazyk-clojure-11-generatorova-notace-seznamu-list-comprehension/
  12. 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/
  13. 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/
  14. Clojure 14: Základy práce se systémem maker
    http://www.root.cz/clanky/programovaci-jazyk-clojure-14-zaklady-prace-se-systemem-maker/
  15. Clojure 15: Tvorba uživatelských maker
    http://www.root.cz/clanky/programovaci-jazyk-clojure-15-tvorba-uzivatelskych-maker/
  16. Clojure 16: Složitější uživatelská makra
    http://www.root.cz/clanky/programovaci-jazyk-clojure-16-slozitejsi-uzivatelska-makra/
  17. Clojure 17: Využití standardních maker v praxi
    http://www.root.cz/clanky/programovaci-jazyk-clojure-17-vyuziti-standardnich-maker-v-praxi/
  18. Clojure 18: Základní techniky optimalizace aplikací
    http://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/
  19. Clojure 19: Vývojová prostředí pro Clojure
    http://www.root.cz/clanky/programovaci-jazyk-clojure-19-vyvojova-prostredi-pro-clojure/
  20. 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/
  21. Clojure 21: ClojureScript aneb překlad Clojure do JS
    http://www.root.cz/clanky/programovaci-jazyk-clojure-21-clojurescript-aneb-preklad-clojure-do-javascriptu/
  22. Leiningen: nástroj pro správu projektů napsaných v Clojure
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/
  23. 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/
  24. 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/
  25. 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/
  26. 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/
  27. 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/
  28. Programovací jazyk Clojure a databáze (1.část)
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-databaze-1-cast/
  29. Pluginy pro Leiningen
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-pluginy-pro-leiningen/
  30. 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/
  31. 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/
  32. 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/
  33. 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/
  34. 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/
  35. Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure
    http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure/
  36. Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure (2)
    http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-2/
  37. 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/
  38. Programovací jazyk Clojure a práce s Gitem
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem/
  39. Programovací jazyk Clojure a práce s Gitem (2)
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem-2/
  40. Programovací jazyk Clojure – triky při práci s řetězci
    http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-retezci/
  41. Programovací jazyk Clojure – triky při práci s kolekcemi
    http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-kolekcemi/
  42. Programovací jazyk Clojure – práce s mapami a množinami
    http://www.root.cz/clanky/programovaci-jazyk-clojure-prace-s-mapami-a-mnozinami/
  43. Programovací jazyk Clojure – základy zpracování XML
    http://www.root.cz/clanky/programovaci-jazyk-clojure-zaklady-zpracovani-xml/
  44. Programovací jazyk Clojure – testování s využitím knihovny Expectations
    http://www.root.cz/clanky/programovaci-jazyk-clojure-testovani-s-vyuzitim-knihovny-expectations/
  45. 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/
  46. Enlive – výkonný šablonovací systém pro jazyk Clojure
    http://www.root.cz/clanky/enlive-vykonny-sablonovaci-system-pro-jazyk-clojure/
  47. 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/
  48. Novinky v Clojure verze 1.8.0
    http://www.root.cz/clanky/novinky-v-clojure-verze-1–8–0/
  49. 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/

12. Odkazy na Internetu

  1. Communicating sequential processes
    https://en.wikipedia.org/wi­ki/Communicating_sequenti­al_processes
  2. Clojure core.async
    http://www.infoq.com/presen­tations/clojure-core-async
  3. core.async API Reference
    https://clojure.github.io/core.async/
  4. Clojure core.async Channels
    http://clojure.com/blog/2013/06/28/clo­jure-core-async-channels.html
  5. core.async examples
    https://github.com/clojure/co­re.async/blob/master/exam­ples/walkthrough.clj
  6. Timothy Baldridge – Core.Async
    https://www.youtube.com/wat­ch?v=enwIIGzhahw
  7. Designing Front End Applications with core.async
    http://go.cognitect.com/co­re_async_webinar_recording
  8. Mastering Concurrent Processes with core.async
    http://www.braveclojure.com/core-async/
  9. LispCast: Clojure core.async
    https://www.youtube.com/wat­ch?v=msv8Fvtd6YQ
  10. Julian Gamble – Applying the paradigms of core.async in ClojureScript
    https://www.youtube.com/wat­ch?v=JUrOebC5HmA
  11. Zip archiv s Clojure 1.8.0
    http://repo1.maven.org/ma­ven2/org/clojure/clojure/1­.8.0/clojure-1.8.0.zip
  12. Clojure 1.8 is now available
    http://clojure.org/news/2016/01/19/clo­jure18
  13. Changes to Clojure in Version 1.8
    https://github.com/clojure/clo­jure/blob/master/changes.md
  14. Socket Server REPL
    http://dev.clojure.org/dis­play/design/Socket+Server+REPL
  15. CLJ-1671: Clojure socket server
    http://dev.clojure.org/jira/browse/CLJ-1671
  16. CLJ-1449: Add clojure.string functions for portability to ClojureScript
    http://dev.clojure.org/jira/browse/CLJ-1449
  17. Launching a Socket Server
    http://clojure.org/referen­ce/repl_and_main#_launchin­g_a_socket_server
  18. API for clojure.string
    http://clojure.github.io/clo­jure/branch-master/clojure.string-api.html
  19. Clojars:
    https://clojars.org/
  20. Seznam knihoven na Clojars:
    https://clojars.org/projects
  21. Clojure Cookbook: Templating HTML with Enlive
    https://github.com/clojure-cookbook/clojure-cookbook/blob/master/07_webapps/7–11_enlive.asciidoc
  22. An Introduction to Enlive
    https://github.com/swannodette/enlive-tutorial/
  23. Enlive na GitHubu
    https://github.com/cgrand/enlive
  24. Expectations: příklady atd.
    http://jayfields.com/expectations/
  25. Expectations na GitHubu
    https://github.com/jaycfi­elds/expectations
  26. Lein-expectations na GitHubu
    https://github.com/gar3thjon3s/lein-expectations
  27. Testing Clojure With Expectations
    https://semaphoreci.com/blog/2014/09/23/tes­ting-clojure-with-expectations.html
  28. Clojure testing TDD/BDD libraries: clojure.test vs Midje vs Expectations vs Speclj
    https://www.reddit.com/r/Clo­jure/comments/1viilt/cloju­re_testing_tddbdd_librari­es_clojuretest_vs/
  29. Testing: One assertion per test
    http://blog.jayfields.com/2007/06/tes­ting-one-assertion-per-test.html
  30. Rewriting Your Test Suite in Clojure in 24 hours
    http://blog.circleci.com/rewriting-your-test-suite-in-clojure-in-24-hours/
  31. Clojure doc: zipper
    http://clojuredocs.org/clo­jure.zip/zipper
  32. Clojure doc: parse
    http://clojuredocs.org/clo­jure.xml/parse
  33. Clojure doc: xml-zip
    http://clojuredocs.org/clojure.zip/xml-zip
  34. Clojure doc: xml-seq
    http://clojuredocs.org/clo­jure.core/xml-seq
  35. Parsing XML in Clojure
    https://github.com/clojuredocs/guides
  36. Clojure Zipper Over Nested Vector
    https://vitalyper.wordpres­s.com/2010/11/23/clojure-zipper-over-nested-vector/
  37. Understanding Clojure's PersistentVector implementation
    http://blog.higher-order.net/2009/02/01/understanding-clojures-persistentvector-implementation
  38. Understanding Clojure's PersistentHashMap (deftwice…)
    http://blog.higher-order.net/2009/09/08/understanding-clojures-persistenthashmap-deftwice.html
  39. Assoc and Clojure's PersistentHashMap: part ii
    http://blog.higher-order.net/2010/08/16/assoc-and-clojures-persistenthashmap-part-ii.html
  40. Ideal Hashtrees (paper)
    http://lampwww.epfl.ch/pa­pers/idealhashtrees.pdf
  41. Clojure home page
    http://clojure.org/
  42. Clojure (downloads)
    http://clojure.org/downloads
  43. Clojure Sequences
    http://clojure.org/sequences
  44. Clojure Data Structures
    http://clojure.org/data_structures
  45. The Structure and Interpretation of Computer Programs: 2.2.1 Representing Sequences
    http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-15.html#%_sec2.2.1
  46. The Structure and Interpretation of Computer Programs: 3.3.1 Mutable List Structure
    http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-22.html#%_sec3.3.1
  47. Clojure – Functional Programming for the JVM
    http://java.ociweb.com/mar­k/clojure/article.html
  48. Clojure quick reference
    http://faustus.webatu.com/clj-quick-ref.html
  49. 4Clojure
    http://www.4clojure.com/
  50. ClojureDoc (rozcestník s dokumentací jazyka Clojure)
    http://clojuredocs.org/
  51. Clojure (na Wikipedia EN)
    http://en.wikipedia.org/wiki/Clojure
  52. Clojure (na Wikipedia CS)
    http://cs.wikipedia.org/wiki/Clojure
  53. SICP (The Structure and Interpretation of Computer Programs)
    http://mitpress.mit.edu/sicp/
  54. Pure function
    http://en.wikipedia.org/wi­ki/Pure_function
  55. Funkcionální programování
    http://cs.wikipedia.org/wi­ki/Funkcionální_programová­ní
  56. Čistě funkcionální (datové struktury, jazyky, programování)
    http://cs.wikipedia.org/wi­ki/Čistě_funkcionální
  57. 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
  58. Clojure Macro Tutorial (Part II: The Compiler Strikes Back)
    http://www.learningclojure­.com/2010/09/clojure-macro-tutorial-part-ii-compiler.html
  59. Clojure Macro Tutorial (Part III: Syntax Quote)
    http://www.learningclojure­.com/2010/09/clojure-macro-tutorial-part-ii-syntax.html
  60. Tech behind Tech: Clojure Macros Simplified
    http://techbehindtech.com/2010/09/28/clo­jure-macros-simplified/
  61. Fatvat – Exploring functional programming: Clojure Macros
    http://www.fatvat.co.uk/2009/02/clo­jure-macros.html
  62. Eulerovo číslo
    http://cs.wikipedia.org/wi­ki/Eulerovo_číslo
  63. List comprehension
    http://en.wikipedia.org/wi­ki/List_comprehension
  64. List Comprehensions in Clojure
    http://asymmetrical-view.com/2008/11/18/list-comprehensions-in-clojure.html
  65. Clojure Programming Concepts: List Comprehension
    http://en.wikibooks.org/wi­ki/Clojure_Programming/Con­cepts#List_Comprehension
  66. Clojure core API: for macro
    http://clojure.github.com/clo­jure/clojure.core-api.html#clojure.core/for
  67. cirrus machina – The Clojure for macro
    http://www.cirrusmachina.com/blog/com­ment/the-clojure-for-macro/
  68. Riastradh's Lisp Style Rules
    http://mumble.net/~campbe­ll/scheme/style.txt
  69. Dynamic Languages Strike Back
    http://steve-yegge.blogspot.cz/2008/05/dynamic-languages-strike-back.html
  70. Scripting: Higher Level Programming for the 21st Century
    http://www.tcl.tk/doc/scripting.html
  71. Java Virtual Machine Support for Non-Java Languages
    http://docs.oracle.com/ja­vase/7/docs/technotes/gui­des/vm/multiple-language-support.html
  72. Třída java.lang.String
    http://docs.oracle.com/ja­vase/7/docs/api/java/lang/Strin­g.html
  73. Třída java.lang.StringBuffer
    http://docs.oracle.com/ja­vase/7/docs/api/java/lang/Strin­gBuffer.html
  74. Třída java.lang.StringBuilder
    http://docs.oracle.com/ja­vase/7/docs/api/java/lang/Strin­gBuilder.html
  75. StringBuffer versus String
    http://www.javaworld.com/ar­ticle/2076072/build-ci-sdlc/stringbuffer-versus-string.html
  76. Threading macro (dokumentace k jazyku Clojure)
    https://clojure.github.io/clo­jure/clojure.core-api.html#clojure.core/->
  77. Understanding the Clojure → macro
    http://blog.fogus.me/2009/09/04/un­derstanding-the-clojure-macro/
  78. clojure.inspector
    http://clojure.github.io/clo­jure/clojure.inspector-api.html
  79. The Clojure Toolbox
    http://www.clojure-toolbox.com/
  80. Unit Testing in Clojure
    http://nakkaya.com/2009/11/18/unit-testing-in-clojure/
  81. Testing in Clojure (Part-1: Unit testing)
    http://blog.knoldus.com/2014/03/22/tes­ting-in-clojure-part-1-unit-testing/
  82. API for clojure.test – Clojure v1.6 (stable)
    https://clojure.github.io/clo­jure/clojure.test-api.html
  83. Leiningen: úvodní stránka
    http://leiningen.org/
  84. Leiningen: Git repository
    https://github.com/techno­mancy/leiningen
  85. leiningen-win-installer
    http://leiningen-win-installer.djpowell.net/
  86. Clojure.org: Vars and the Global Environment
    http://clojure.org/Vars
  87. Clojure.org: Refs and Transactions
    http://clojure.org/Refs
  88. Clojure.org: Atoms
    http://clojure.org/Atoms
  89. Clojure.org: Agents as Asynchronous Actions
    http://clojure.org/agents
  90. Transient Data Structureshttp://clojure.or­g/transients
Našli jste v článku chybu?
Měšec.cz: Cestujte bez starostí, získejte výhodné pojištění

Cestujte bez starostí, získejte výhodné pojištění

DigiZone.cz: Deklarace kompatibility? Jen LG...

Deklarace kompatibility? Jen LG...

Lupa.cz: Vzali věc, která fungovala, a přidali internet

Vzali věc, která fungovala, a přidali internet

Lupa.cz: Vydavatelé jsou v háji, ale neumí si to připustit

Vydavatelé jsou v háji, ale neumí si to připustit

Měšec.cz: Udali ho na nelegální software a přišla Policie

Udali ho na nelegální software a přišla Policie

DigiZone.cz: Pardubicko: Výrazně posílen Mux 3

Pardubicko: Výrazně posílen Mux 3

DigiZone.cz: Slováci první, Češi třetí. Krásný...

Slováci první, Češi třetí. Krásný...

DigiZone.cz: Noxon iRadio 1 W bude za pár měsíců

Noxon iRadio 1 W bude za pár měsíců

Vitalia.cz: Jíme přesolené potraviny. Zrovna tyhle

Jíme přesolené potraviny. Zrovna tyhle

Vitalia.cz: 5 porcí ovoce a zeleniny: no ale jak na to?

5 porcí ovoce a zeleniny: no ale jak na to?

DigiZone.cz: Robinsonův ostrov moderuje Novotný

Robinsonův ostrov moderuje Novotný

Podnikatel.cz: Výpadek internetu a #EET. Co s tím?

Výpadek internetu a #EET. Co s tím?

120na80.cz: Krémy, nebo spreje na opalování?

Krémy, nebo spreje na opalování?

Vitalia.cz: 7 receptur z pohanky. Svědčí zdraví

7 receptur z pohanky. Svědčí zdraví

Lupa.cz: Nej aplikace? Vodafone, Mozkovna, Záchranka

Nej aplikace? Vodafone, Mozkovna, Záchranka

DigiZone.cz: Kanály Novy na Slovensku oficiálně?

Kanály Novy na Slovensku oficiálně?

DigiZone.cz: HbbTV KinoSvět: už jede na dalších TV

HbbTV KinoSvět: už jede na dalších TV

Podnikatel.cz: Takhle si Babiš představuje nové daně

Takhle si Babiš představuje nové daně

DigiZone.cz: Satelitní Flix TV vyráží do boje

Satelitní Flix TV vyráží do boje

120na80.cz: Léky a dietní opatření při kopřivce

Léky a dietní opatření při kopřivce