Obsah
1. Programovací jazyk Pixie: funkce ze základní knihovny a použití FFI
4. Opakování části kódu s použitím konstrukce dotimes
5. Průchod prvky sekvence s využitím doseq
6. Generátorová notace seznamu – list comprehension
7. Praktický příklad – výpočet Eulerova čísla s využitím doseq, for a reduce
8. Použití programové konstrukce loop/recur
9. Použití FFI aneb volání nativních funkcí
10. Nativní funkce dostupné ze standardních modulů jazyka Pixie
11. Příklad – vygenerování perexového obrázku k tomuto článku
12. Problém s nativními funkcemi typu printf
13. Odkazy na předchozí části tohoto seriálu
1. Programovací jazyk Pixie: funkce ze základní knihovny a použití FFI
V úvodním článku o programovacím jazyku Pixie jsme se seznámili se základními vlastnostmi tohoto programovacího jazyka. Připomeňme si, že Pixie se po stránce syntaxe a z velké části i sémantiky podobá jazyku Clojure, ovšem s tím rozdílem, že zatímco Clojure je určen pro běh nad nějakým větším a univerzálnějším virtuálním strojem (JVM, CLR, VM JavaScriptu), má programovací jazyk Pixie vlastní (výrazně menší) virtuální stroj využívající PyPy. Dnes se nejprve seznámíme s dalšími funkcemi a makry ze základní knihovny, které jsou určeny pro implementaci programových smyček, tail rekurze či generátorové notace seznamu (list comprehension). Ve druhé části se budeme zabývat problematikou FFI, tj. volání nativních funkcí přímo ze skriptů napsaných v jazyce Pixie.
2. Použití klasické rekurze
V programovacím jazyku Pixie se, podobně jako v mnoha dalších programovacích jazycích, většinou nepoužívají klasické programové smyčky. Namísto nich se využívají funkce vyššího řádu (map, reduce, filter atd.), generátorová notace seznamu představovaná makrem for, klasická přímá rekurze nebo tzv. tail rekurze. Nejprve si ukažme použití přímé rekurze, tj. takového zápisu, kdy se v těle vytvářené funkce může objevit volání této funkce. Zcela typickým „školním“ příkladem rekurzivní funkce je funkce pro výpočet faktoriálu, jejíž jednoduchá varianta (neochráněná před všemi typy vstupů) může vypadat takto:
user=> (defn fact [n] (if (<= n 1) 1 (* n (fact (- n 1))))) #'user/fact
Funkci lze samozřejmě přepsat do čitelnějšího tvaru:
(defn fact [n] (if (<= n 1) 1 (* n (fact (- n 1)))))
Podobným způsobem je možné rekurzivně implementovat funkci pro výpočet největšího společného dělitele dvou přirozených čísel. V následující implementaci schválně nepoužívám výpočet zbytku po dělení, ale pouze postupný výpočet rozdílu mezivýsledků:
(defn gcd [x y] (if (= x y) x (if (> x y) (gcd (- x y) y) (gcd x (- y x)))))
Opět si zkusme tuto funkci otestovat. Pro malé vstupní hodnoty (přirozená čísla!) pracuje bez problémů:
user => (gcd 1 1) 1 user => (gcd 1 2) 1 user => (gcd 4 2) 2 user => (gcd 6 9) 3 user => (gcd 12 8) 4 user => (gcd 1024 768) 256
Co se však stane pro větší vstupy?:
user => (gcd 123456 98765) ERROR: RuntimeException: :pixie.stdlib/InternalError Internal error: <StackOverflow object at 0xccc6f8>
Vidíme, že zápis algoritmu přímou rekurzí sice může být elegantní a může vycházet přímo z definice řešení problému (viz faktoriál, ovšem i spousta dalších funkcí je v matematice definována rekurzivně), ale z praktického hlediska zde evidentně existují technická omezení.
3. Použití tail rekurze
Důvod, proč předchozí volání funkce gcd skončilo s chybou, spočívá v tom, že došlo k přeplnění zásobníku při rekurzivním volání. Na zásobník se totiž musí v takzvané winding fázi ukládat parametry předávané volané funkci a taktéž body návratu (zjednodušeně řečeno návratové adresy). Aby k přetečení zásobníku nedocházelo, můžeme naši funkci gcd upravit tak, aby se využívalo takzvané tail rekurze. Velmi zjednodušeně řečeno je tail rekurze použita tehdy, pokud je posledním příkazem nějaké funkce příkaz pro rekurzivní volání té samé funkce. V tomto případě se nemusí na zásobník nic ukládat a namísto toho se prostě provede skok. V jazyku Pixie se však musí tail rekurze zapsat explicitně, což má své přednosti i zápory (podle mě převažují přednosti, protože již ze zápisu programu je zcela zřejmé, kdy k tail rekurzi skutečně dojde).
Explicitní zápis rekurze spočívá ve využití speciální formy recur, která se zapíše přesně do místa, kde má k tail rekurzi (=skoku) dojít. U funkce gcd je použití přímočaré, prostě se na posledních dvou řádcích nahradí (gcd parametry) za (recur parametry):
(defn gcd [x y] (if (= x y) x (if (> x y) (recur (- x y) y) (recur x (- y x)))))
Chování si můžeme ihned otestovat na příkladu, který v případě zápisu algoritmu přímou rekurzí zhavaroval:
user => (gcd 123456 98765) 1
Ještě větší hodnoty také nezpůsobí žádné problémy:
user => (gcd 120000000000 800224) 32
Mohlo by se zdát, že by překladač programovacího jazyka Pixie snad bylo možné zmást a použít recur jako náhradu skoku kdekoli. Ve skutečnosti si však překladač hlídá, zda je recur použito správně:
(defn fact [n] (if (<= n 1) 1 (* n (recur (- n 1)))))
Překladač vypíše následující (zcela korektní) chybové hlášení:
ERROR: in pixie function repl_fn in pixie/repl.pxi at 27:24 (let [x (eval form)] ^ in internal function eval RuntimeException: :pixie.stdlib/AssertionException Can't recur in non-tail position
Funkci fact je totiž nutné nejdříve upravit tak, aby rekurzivní volání skutečně nepotřebovalo zásobník. Například lze použít akumulátor a funkci s více aritami (zjednodušeně řečeno – fact lze volat s jedním parametrem či se dvěma parametry, přičemž druhým parametrem bude mezihodnota uložená do akumulátoru):
(defn fact ([n] (fact n 1)) ([n acc] (if (<= n 1) acc (recur (dec n) (* acc n)))))
Nyní již vše bude fungovat bez přetečení zásobníku:
user =< (fact 1000) 4023872600770937735437024339230039857193748642107146325437999104 2993851239862902059204420848696940480047998861019719605863166687 2994808558901323829669944590997424504087073759918823627727188732 5197795059509952761208749754624970436014182780946464962910563938 8743788648733711918104582578364784997701247663288983595573543251 3185323958463075557409114262417474349347553428646576611667797396 6688202912073791438537195882498081268678383745597317461360853795 3452422158659320192809087829730843139284440328123155861103697680 1357304216168747609675871348312025478589320767169132448426236131 4125087802080002616831510273418279777047846358681701643650241536 9139828126481021309276124489635992870511496497541990934222156683 2572080821333186116811553615836546984046708975602900950537616475 8477284218896796462449451607653534081989013854424879849599533191 0172335555660213945039973628075013783761530712776192684903435262 5200015888535147331611702103968175921510907788019393178114194545 2572238655414610628921879602238389714760885062768629671466746975 6291123408243920816015378088989396451826324367161676217916890977 9911903754031274622289988005195444414282012187361745992642956581 7466283029555702990243241531816172104658320367869061172601587835 2075151628422554026517048330422614397428693306169089796848259012 5458327168226458066526769958652682272807075781391858178889652208 1643483448259932660433676601769996128318607883861502794659551311 5655203609398818061213855860030143569452722420634463179746059468 2573103790084024432438465657245014402821885252470935190620929023 1364932734975655139587205596542287497740114133469627154228458623 7738753823048386568897646192738381490014076731044664025989949022 2221765904339901886018566526485061799702356193897017860040811889 7299183110211712298459016419210688843871218556461249607987229085 1929681937238864261483965738229112312502418664935314397013742853 1926649875337218940694281434118520158014123344828015051399694290 1534830776445690990731524332782882698646027898643211390835062170 9500259738986355427719674282224875758676575234422020757363056949 8825087968928162753848863396909959826280956121450994871701244516 4612603790293091208890869420285106401821543994571568059418727489 9809425474217358240106367740459574178516082923013535808184009699 6372524230560855903700624271243416909004153690105933983835777939 4109700277534720000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 00000000N
4. Opakování části kódu s použitím konstrukce dotimes
V některých částech programu (ve skutečnosti jich však není mnoho) může být poměrně užitečné makro nazvané jednoduše a přitom příhodně dotimes, které dokáže nějaký výraz (formu) opakovat n krát. Přitom toto makro může v každé iteraci (opakování) nastavit zvolenou lokální proměnnou na aktuální hodnotu počitadla, přičemž se hodnota počitadla v první iteraci vždy nastavuje na nulu a v poslední iteraci dosahuje zadaného počtu opakování-1:
user => (doc dotimes) pixie.stdlib/dotimes ([[i n] & body]) Executes the expressions in the body n times. user => (dotimes [i 3] (println i)) 1 2 3
Vzdáleně tedy můžeme toto makro považovat za ekvivalent programové smyčky for i in range(n): v programovacím jazyku Python či ekvivalent k počítané smyčce for (int i = 0; i<n; i++) známé z céčka (zde bez možnosti mít lokální proměnnou jako počitadlo), C++, Javy atd. Vzhledem k tomu, že se předpokládá, že forma – tělo smyčky – předaná makru dotimes bude mít nějaký vedlejší efekt, nejedná se sice o čistě funkcionální přístup, nicméně se někdy s použitím makra dotimes můžeme setkat (například v testech).
V jednoduchém demonstračním příkladu, který si ukážeme, se na standardní výstup vypisuje převrácená hodnota celých čísel od 0 do 9. Vedlejším efektem je v tomto případě samotný výpis na standardní výstup funkcí println:
user => (dotimes [i 10] (println (/ 1.0 i))) inf 1.0 0.5 0.3333333333333333 0.25 0.2 0.16666666666666666 0.14285714285714285 0.125 0.1111111111111111 nil
user => (/ 1. 0) inf user => (/ -1. 0) -inf user => (/ 0. 0) nan
Podívejme se nyní na nepatrně složitější příklad, který by se v imperativních programovacích jazycích většinou řešil s využitím dvojice do sebe vnořených počítaných programových smyček. Mějme za úkol vypsat tabulku malé násobilky, tj. všechny výsledky vzniklé vynásobením dvojic celých čísel od 1 do 10. Tento algoritmus je možné velmi snadno realizovat právě s využitím makra dotimes, například následujícím one-linerem:
user => (dotimes [i 10] (dotimes [j 10] (print (* (+ i 1) (+ j 1)) "\t")) (println))
Malou „optimalizací“ v tomto zápisu by byla náhrada výrazů (+ i 1) za volání funkce (inc i) s prakticky shodným významem (alespoň pro čísla typu integer). Mimochodem: jméno funkce inc může evokovat to, že zvyšuje hodnotu svého parametru, což však ve skutečnosti ani není možné, neboť se jedná o běžnou funkci a nikoli o makro či o speciální formu (důležitá je tedy návratová hodnota této funkce):
user => (dotimes [i 10] (dotimes [j 10] (print (* (inc i) (inc j)) "\t")) (println))
Pro větší přehlednost si můžeme výše uvedený one-liner přepsat na správně odsazený program, z něhož je patrné, že se skutečně jedná o ekvivalent dvou do sebe zanořených programových smyček:
(dotimes [i 10] (dotimes [j 10] (print (* (inc i) (inc j)) "\t")) (println))
A zde je již výsledek práce tohoto prográmku (poslední nil je opět návratovou hodnotou makra dotimes):
1 2 3 4 5 6 7 8 9 10 2 4 6 8 10 12 14 16 18 20 3 6 9 12 15 18 21 24 27 30 4 8 12 16 20 24 28 32 36 40 5 10 15 20 25 30 35 40 45 50 6 12 18 24 30 36 42 48 54 60 7 14 21 28 35 42 49 56 63 70 8 16 24 32 40 48 56 64 72 80 9 18 27 36 45 54 63 72 81 90 10 20 30 40 50 60 70 80 90 100 nil
Poznámka: na rozdíl od dále uvedené formy for není možné v makru dotimes použít kombinaci dvou či většího množství počitadel. Následující zápis je tedy bude ignorovat pokus o vytvoření počitadla j:
user => (dotimes [i 10 j 20] (println i)) 0 1 2 3 4 5 6 7 8 9 nil user => (dotimes [i 10 j 20] (println j)) ERROR: in pixie function repl_fn in pixie/repl.pxi at 27:24 (let [x (eval form)] ^ in internal function eval in <unknown> at 13:31 (dotimes [i 10 j 20] (println j)) ^ RuntimeException: :pixie.stdlib/AssertionException Var j is undefined
5. Průchod prvky sekvence s využitím doseq
Mnohem častěji než s výše uvedeným makrem dotimes se setkáme s makrem doseq, které lze použít pro postupné procházení všemi prvky zvolené sekvence (a tím pádem i kolekce):
user => (doc doseq) pixie.stdlib/doseq ([binding & body]) Evaluates all elements of the seq, presumably for side effects. Returns nil.
Použití tohoto makra je velmi jednoduché, což je ostatně patrné i z následujících příkladů.
Průchod prvky vektoru:
user => (doseq [i ["a" "b" "c" "d"]] (println i)) a b c d
Podobný příklad, ovšem pro seznam:
user => (doseq [i '("a" "b" "c" "d")] (println i)) a b c d
Průchod všemi prvky množiny (prvky se do množiny obecně ukládají neuspořádaně):
user => (doseq [i #{1 2 3 4 5 6}] (print i " ")) 4 6 2 5 3 1 nil
Použití pro generované sekvence (ve skutečnosti by bylo mnohem vhodnější tyto příklady přepsat tak, aby se používala funkce vyššího řádu map či makro for):
user => (doseq [dot (repeat 100 ".")] (print dot)) .................................................................................................... user => (doseq [i (range 10)] (println (/ i (inc i)))) 0 1/2 2/3 3/4 4/5 5/6 6/7 7/8 8/9 9/10 nil
Poznámka: pokud stojíte před takovým problémem, který vyžaduje průchod nějakou sekvencí (či kolekcí) s tím, že výsledkem má být jiná sekvence, není použití makra doseq vhodné, protože to se používá jen tehdy, pokud vyžadujete nějaký vedlejší efekt (zde například volání print či println). Při zpracování jedné sekvence na sekvenci jinou je vhodnější použít for, někdy též map, filter či reduce. Jinými slovy – nenechte se prosím zmást tím, že v některých jiných programovacích jazycích se veškeré průchody sekvencemi provádí jediným typem smyčky for-each, když v Clojure/Pixie existují pro každý typ zpracování specializované funkce (= low-level návrhové vzory).
6. Generátorová notace seznamu – list comprehension
Programovací jazyk Pixie převzal z Clojure i makro for, s jehož využitím je možné používat takzvanou „generátorovou notaci seznamu“, což je poněkud nepřesně přeložený anglický termín „list comprehension“. O způsobu překladu tohoto termínu se vedly a pravděpodobně dodnes vedou poměrně vášnivé diskuse; já se zde budu držet překladu použitého v knize Ponořme se do Python(u) 3 (původně Dive Into Python 3)), kterou si můžete stáhnout na stránkách http://knihy.nic.cz/, popř. si zde objednat i papírovou verzi knihy.
S využitím generátorové notace seznamu je možné v programovacích jazycích, které tento zápis podporují, zapsat deklaraci vytvoření nového seznamu s využitím seznamu jiného, a to pomocí aplikace nějaké zvolené funkce na všechny prvky zdrojového seznamu. V mnoha programovacích jazycích je nutné pro generátorovou notaci seznamů používat zvláštní příkaz či deklaraci, tj. vlastně novou syntaktickou kategorii. Z těch známějších jazyků podporujících list comprehension se jedná například o Python, Haskell, Scala či Erlang.
Programovací jazyk Clojure a tím pádem i Pixie se však od těchto jazyků v podpoře pro generátorovou notaci seznamů liší, a to dokonce hned v několika ohledech. V Clojure i Pixie se totiž nemusela zavádět žádná nová syntaxe, ale postačovalo vhodně naprogramovat makro (jeho kód je však poměrně dlouhý). Navíc je možné aplikovat zvolenou funkci nejenom na (konečný) seznam, ale na libovolnou (a to i nekonečnou) sekvenci, čímž ve výsledku můžeme taktéž získat nekonečnou sekvenci vyhodnocovanou líně (lazy sequence). Třetím rozdílem Clojure a Pixie oproti jiným jazykům s podporou generátorových notací seznamů je to, že je možné pracovat s libovolným množstvím zdrojových seznamů (přesněji řečeno sekvencí).
Generátorová notace seznamů je implementována již výše zmíněným makrem for:
user => (doc for) pixie.stdlib/for ([bindings & body]) A list comprehension for the bindings. user => (for [x [1 2 3]] x) [1 2 3] user => (for [x [1 2 3] y [:a :b :c]] [x y]) [[1 :a] [1 :b] [1 :c] [2 :a] [2 :b] [2 :c] [3 :a] [3 :b] [3 :c]]
Zajímavý je především způsob zápisu makra for, kde je v hranatých závorkách (tedy ve vektoru) nejprve uveden název lokální proměnné používané při výpočtu a za jménem proměnné je pak uveden zdrojový seznam. Apostrof před zdrojovým seznamem zabrání jeho vyhodnocení; pokud by zde nebyl uveden, došlo by k pokusu o zavolání funkce s názvem 1 s trojicí parametrů 2, 3 4. Posledním parametrem makra for je výraz (forma), jehož výsledek je použit pro konstrukci výsledného seznamu:
user=> (for [x '(1 2 3 4)] (inc x)) (2 3 4 5)
Lokální proměnnou x, která je postupně navazována na jednotlivé prvky zdrojového seznamu, je samozřejmě možné použít i ve složitějších výrazech; důležitý je pro nás pouze výsledek daného výrazu (jen minimálně se zde používají funkce s vedlejším efektem, i když i to je možné).
Kromě seznamu lze použít i libovolnou jinou sekvenci či kolekci (vektor, mapu, množinu):
user => (for [x [1 2 3 4]] (inc x)) (2 3 4 5) user => (for [x #{1 2 3 4}] (inc x)) (5 3 4 2) user => (for [x {:a 1 :b 2 :c 3 :d 4}] (key x)) (:b :c :a :d) user => (for [x (range 1 10)] (/ 3 x)) (3 3/2 1 3/4 3/5 1/2 3/7 3/8 1/3)
for je samozřejmě možné libovolným způsobem zanořovat, i když čitelnost je v tomto případě již velmi malá (lepší je rozdělení zápisu na více řádků):
user=> (for [x (for [x (range 1 10)] (inc x))] (* x 2)) (4 6 8 10 12 14 16 18 20)
for dokáže pracovat s více generátory (tj. s více zdrojovými sekvencemi) současně. Princip jeho práce se podobá funkci vnořených programových smyček typu „for“. Podívejme se nyní, jak se vlastně makro for s více generátory zapisuje. Nejjednodušší způsob zápisu vypadá následovně:
user=> (for [x (range 1 10) y (range 1 10)] (* x y)) (1 2 3 4 5 6 7 8 9 2 4 6 8 10 12 14 16 18 3 6 9 12 15 18 21 24 27 4 8 12 16 20 24 28 32 36 5 10 15 20 25 30 35 40 45 6 12 18 24 30 36 42 48 54 7 14 21 28 35 42 49 56 63 8 16 24 32 40 48 56 64 72 9 18 27 36 45 54 63 72 81)
My ovšem můžeme taktéž vytvořit sekvenci obsahující dvojice, trojice atd. prvků. Postačuje jen vhodně upravit funkci volanou makrem for, například takovým způsobem, aby se vytvořit vektor obsahující dvojici čísel:
user=> (for [x (range 1 10) y (range 1 10)] [x y]) ( [1 1] [1 2] [1 3] [1 4] [1 5] [1 6] [1 7] [1 8] [1 9] [2 1] [2 2] [2 3] [2 4] [2 5] [2 6] [2 7] [2 8] [2 9] [3 1] [3 2] [3 3] [3 4] [3 5] [3 6] [3 7] [3 8] [3 9] [4 1] [4 2] [4 3] [4 4] [4 5] [4 6] [4 7] [4 8] [4 9] [5 1] [5 2] [5 3] [5 4] [5 5] [5 6] [5 7] [5 8] [5 9] [6 1] [6 2] [6 3] [6 4] [6 5] [6 6] [6 7] [6 8] [6 9] [7 1] [7 2] [7 3] [7 4] [7 5] [7 6] [7 7] [7 8] [7 9] [8 1] [8 2] [8 3] [8 4] [8 5] [8 6] [8 7] [8 8] [8 9] [9 1] [9 2] [9 3] [9 4] [9 5] [9 6] [9 7] [9 8] [9 9])
7. Praktický příklad – výpočet Eulerova čísla s využitím doseq, for a reduce
Pojďme si nyní ukázat použití funkcí a maker doseq, for a reduce v jednoduchém programu pro výpočet přibližné hodnoty Eulerova čísla, které tvoří základ přirozených logaritmů a má v matematice jako jedna ze základních konstant i mnoho dalších použití. Pro výpočet Eulerova čísla použijeme součet nekonečné číselné řady prvků s hodnotou 1/x!. Celý vzorec lze nalézt na stránce http://cs.wikipedia.org/wiki/Eulerovo_číslo ve formě druhé definice této konstanty.
Aby bylo možné provést součet prvků výše zmíněné nekonečné řady, musíme nejprve deklarovat funkci pro výpočet faktoriálu. Některé způsoby zápisu této funkce jsme si již ukazovali – jednalo se jak o čistě rekurzivní zápis, tak i o zápis používající tail rekurzi, popř. programovou smyčku. Nyní se pro jednoduchost spolehneme na použití funkce reduce, která bude redukovat vypočtenou sekvenci celočíselných hodnot (aritmetickou řadu celých čísel od 1 do n) s využitím operace násobení (jedná se o pravděpodobně nejkratší zápis faktoriálu):
(defn fact [n] (reduce * (range 1 (inc n))))
Námi vytvořenou funkci pro výpočet faktoriálu můžeme jednoduše otestovat, například její aplikací na vstupní hodnoty 0..9:
user=> (map fact (range 0 10)) (1 1 2 6 24 120 720 5040 40320 362880)
Funkci fact můžeme použít společně s makrem for pro vytvoření číselné řady obsahující hodnoty 1/x!, přičemž x leží ve zvoleném rozsahu (0..max):
user=> (for [x (range 0 10)] (/ 1 (fact x))) (1 1 1/2 1/6 1/24 1/120 1/720 1/5040 1/40320 1/362880)
Pro výpočet přibližné hodnoty Eulerova čísla nám již pouze postačí výsledné hodnoty sečíst, například takto:
user=> (reduce + (for [x (range 0 10)] (/ 1 (fact x)))) 98641/36288
Samozřejmě je možné si vypsat tabulku pro zvětšující se limit, aby bylo patrné, že se výpočet postupně přibližuje k Eulerovu číslu (pokud ovšem nedojde k přetečení při zjednodušování zlomků!):
(doseq [i (range 1 15)] (println i (reduce + (for [x (range 0 i)] (/ 1 (fact x)))))) 1 1 2 2 3 5/2 4 8/3 5 65/24 6 163/60 7 1957/720 8 685/252 9 109601/40320 10 98641/36288 11 9864101/3628800 12 13563139/4989600 13 260412269/95800320 14 8463398743/3113510400
Zápis výsledků pomocí zlomků nám v tomto případě příliš nepomůže, takže zlomek převedeme na běžnou numerickou hodnotu reprezentovanou v systému plovoucí řádové čárky. Jednou z možností je pouhé přičtení hodnoty 0.0 popř. vynásobení hodnotou 1.0:
(doseq [i (range 1 15)] (println i (+ 0.0 (reduce + (for [x (range 0 i)] (/ 1 (fact x))))))) 1 1.000000 2 2.000000 3 2.500000 4 2.666667 5 2.708333 6 2.716667 7 2.718056 8 2.718254 9 2.718279 10 2.718282 11 2.718282 12 2.718282 13 2.718282 14 2.718282
8. Použití programové konstrukce loop/recur
Poměrně jednoduše je možné rekurzi s využitím TCO (tail call optimization) deklarovat pomocí loop společně s recur. Za formou loop se nachází vektor s deklarací a inicializací lokálních proměnných použitých ve smyčce, na kterou je rekurzivní volání automaticky převedeno. Forma recur umístěná uvnitř loop přesune řízení programu ihned za slovo loop, přičemž parametry předané do recur slouží ke změně hodnot(y) lokálních proměnných. Co to znamená v praxi? Podívejme se na způsob zápisu smyčky, jejíž počitadlo (lokální proměnná i) se zvyšuje od 0 do 10:
; počitadlo od 0 do 10 (loop [i 0] (if (= i 10) ; podmínka pro ukončení smyčky i ; návratová hodnota při splnění podmínky (recur (+ i 1)))) ; rekurze (s TCO) ; v nové iteraci obsahuje i hodnotu staré_i+1
Poznámka: díky tomuto způsobu zápisu si může překladač pohlídat, zda se skutečně může použít TCO.
Smyčka, ve které se používá počitadlo i a současně se počítá i suma sum, bude implementována takto:
; výpočet desáté mocniny dvojky (loop [i 0 sum 1] (if (= i 10) ; podmínka pro ukončení smyčky sum ; návratová hodnota při splnění podmínky (recur (+ i 1) (* sum 2)))) ; rekurze (s TCO) ; v nové iteraci obsahuje i hodnotu staré_i+1 ; a sum hodnotu staré_sum * 2
Podívejme se nyní na implementaci funkce pro výpočet n-té mocniny dvojky:
; funkce pro výpočet n-té mocniny dvojky (defn pow2 [n] (loop [i 0 sum 1] (if (= i n) ; podmínka pro ukončení smyčky sum ; návratová hodnota při splnění podmínky (recur (+ i 1) (* sum 2))))) ; rekurze (s TCO) ; v nové iteraci obsahuje i hodnotu staré_i+1 ; a sum hodnotu staré_sum * 2
Mimochodem: s využitím loop a recur bylo vytvořeno i makro while, jehož kód vypadá následovně (ve skutečnosti toto makro není v Pixie příliš užitečné, na rozdíl od mainstreamových jazyků a jejich programové konstrukce while):
; definice makra while (defmacro while "Repeatedly executes body while test expression is true. Presumes some side-effect will cause test to become false/nil. Returns nil" {:added "1.0"} [test & body] `(loop [] (when ~test ~@body (recur))))
9. Použití FFI aneb volání nativních funkcí
Knihovny jazyka Pixie nejsou v porovnání s některými jinými programovacími jazyky nijak rozsáhlé, což však nemusí vždy vadit, neboť Pixie dokáže volat i nativní funkce přes FFI. Vlastně se zde setkáváme s podobným principem, jaký známe i z Clojure – Clojure dokáže využívat knihovny nabízené VM (JVM, CLR…), Pixie dokáže využívat nativní knihovny nabízené operačním systémem. V obou případech není zapotřebí znovuvynalézat kolo.
Vlastní rozhraní pro FFI je implementováno ve jmenném prostoru ffi-infer, který však většinou není nutné importovat explicitně – to za nás udělá příslušný modul s obalovými funkcemi (pixie.math, pixie.io, pixie.stdlib, pixie.uv, pixie.streams.zip atd.).
Již v základní instalaci Pixie (viz též úvodní část tohoto článku) jsou k dispozici některé základní nativní knihovní funkce z knihovny libc. Pokud některé z těchto funkcí potřebujeme volat, je nejdříve nutné načíst i příslušný jmenný prostor (namespace). Příkladem mohou být matematické funkce, jejichž nativní verze jsou přístupné přes jmenný prostor pixie.math. Jestliže tyto funkce potřebujeme volat, stačí použít direktivu :require uvedenou při specifikaci nového jmenného prostoru (se jmennými prostory se pracuje stejně, jako je tomu v programovacím jazyku Clojure):
test => (ns test (:require [pixie.math :as math])) nil test => (math/abs -10) 10 test => (math/sin 3.1415) 0.000093 test => (math/sin (/ 3.1415 2)) 1.000000 test => math/M_PI 3.141590 test => math/M_SQRT2 1.414210 test => (* math/M_SQRT2 math/M_SQRT2) 1.999990
Alternativně je možné určit, že všechny symboly z načítaného jmenného prostoru budou dostupné i bez nutnosti udání jmenného prefixu:
test => (ns test (:require [pixie.math :refer :all]) nil test => (abs -42) 42 test => M_PI 3.141590 test => M_SQRT2 1.414210 test => (sin M_PI) 0.000003 test => (sin (/ M_PI 2)) 1.000000
10. Nativní funkce dostupné ze standardních modulů jazyka Pixie
Mezi další užitečné funkce, které používají FFI pro volání nativního kódu, patří funkce ze jmenného prostoru io. Kromě klasických funkcí typu spit a slurp zde nalezneme například užitečnou funkci run-command, která nejenže spustí externí příkaz, ale vrátí i jeho výstup (tím se dosti podstatným způsobem liší od funkce exec, která dokáže vrátit jen návratový kód, nikoli výstup):
test => (ns test (:require [pixie.io :refer :all])) nil test => (count (slurp "README.md")) 5961 test => (run-command "date") "Po rij 24 19:34:17 CEST 2016\n" test => (run-command "ls -la") ... dlouhý výstup...
Pokud potřebujete zpracovávat soubory po jednotlivých řádcích (nikoli tedy jako sekvence), mohou se hodit funkce open-read (otevření souboru pro čtení), open-write (otevření souboru pro zápis), read-line (načtení jednoho řádku z otevřeného souboru) atd.:
test => (def fin (open-read "README.md")) <inst pixie.stdlib.Var> test => (read-line fin) "# Pixie" test => (fs_close fin)
Alternativně lze získat sekvenci řádků:
test => (line-seq fin)
Samozřejmě je možné naimportovat větší množství modulů současně:
test => (ns test (:require [pixie.io :refer :all] [pixie.string :as str])) nil
Načtení textového souboru s jeho rozdělením na jednotlivé řádky lze poměrně čitelně vyřešit threading makrem:
(-> "README.md" slurp str/split-lines)
Délky jednotlivých řádků v souboru README.md vypočteme opět s použitím threading makra, tentokrát však musíme dbát na to, že do funkce vyššího řádu map se sekvence předává ve druhém parametru, nikoli v parametru prvním:
test => (->> "README.md" slurp str/split-lines (map #(count %))) (238 7 0 208 0 8 0 166 266 0 11 0 38 0 26 32 69 86 10 25 0 15 0 26 47 46 68 75 0 11 0 23 14 0 191 0 0 20 0 28 0 0 11 0 46 27 0 30 0 0 13 127 0 6 0 33 0 483 0 0 43 0 144 0 112 0 270 0 250 0 10 0 103 84 0 21 23 0 11 18 5 26 0 3 0 0 0 338 0 28 0 374 0 108 0 115 0 29 60 0 15 0 286 0 23 0 363 0 13 181 0 10 0 251 0)
11. Příklad – vygenerování perexového obrázku k tomuto článku
Jako příklad použití některých funkcí z modulů pixie.math a pixie.io (tyto funkce volají svůj nativní protějšek) si ukažme krátký prográmek, který vygeneruje perexový obrázek k tomuto článku. Při výpočtu vektorového obrázku ukládaného do formátu SVG se používají funkce sinus a kosinus, při ukládání obrázku pak funkce spit (opak funkce slurp, těžko říct, proč Rich Hickey zvolil taková libozvučná jména):
(ns logo (:require [pixie.math :refer :all] [pixie.io :refer :all])) (def s 480) (->> (str "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='" s "' height='" s "'>" (loop [i 0 R 255 G 255 B 0 o ""] (let [r (- 128 i) a (/ i 12.) b (+ i 80) x (+ (/ s 2) (* b (cos a))) y (+ (/ s 2) (* b (sin a))) c (str R "," G "," B) p (str "<circle cx='" x "' cy='" y "' r='" r "' ") q (str "fill='rgb(" R "," G "," B ")' style='fill-opacity:.06'/>\n")] (if (< i 128) (recur (inc i) (- R 2) G (+ B 2) (str o p q p "fill='none' stroke='black'/>\n")) o))) "</svg>") (spit "logo.svg"))
Obrázek 1: Vektorový obrazec vytvořený předchozím demonstračním příkladem.
12. Problém s nativními funkcemi typu printf
Volání některých nativních funkcí je – alespoň prozatím – v Pixie nepoužitelné. Jedná se především o funkci printf, což je škoda, protože právě tato funkce je v praxi velmi užitečná. Pro ilustraci se podívejme na to, co se stane při volání této funkce. V případě, že formátovací řetězec neobsahuje žádné formátovací příkazy, bude volání úspěšné a funkce printf dokonce korektně vrátí počet vypsaných znaků:
user => (printf "Hello World\n") Hello World 12
Při pokusu o použití formátovacích znaků a předání parametrů však ke kýženým výsledkům nedojdeme:
user => (printf "%d %d\n" 1 2) 5175984 -959918736 19 user => (printf "result: %f\n" 3.14) result: 0.000000 17 user => (printf "result: %+10d %10.7f\n" 1 1.2) result: +5175984 0.0000000 30 user => (printf "*%s*" "hello") *garbage*8
Tato chyba je vývojářům známá a je popsána ve dvou issues:
- printf doesn't work as expected #420
- ffi
:variadic? true
generated function do not use all arguments supplied #449
13. Odkazy na předchozí části tohoto seriálu
- 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/ - 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 jazyce 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: 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/ - 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 – 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/ - Pixie: lehký skriptovací jazyk s „kouzelnými“ schopnostmi
https://www.root.cz/clanky/pixie-lehky-skriptovaci-jazyk-s-kouzelnymi-schopnostmi/
14. Odkazy na Internetu
- Pixie (repositář na GitHubu)
https://github.com/pixie-lang/pixie - Pixie Installation
https://github.com/pixie-lang/pixie/wiki/Installation - Pixie Libraries
https://github.com/pixie-lang/pixie/wiki/Libraries - Interview with Timothy Baldridge, Pixie's language creator
https://notamonadtutorial.com/indie-languages-interview-pixie-and-timothy-baldridge-cadbc36418dc#.vhbl5rp1c - Pixie (programming language) [Wikipedia]
https://en.wikipedia.org/wiki/Pixie_%28programming_language%29 - FFI or interop with C libraries
https://github.com/pixie-lang/pixie/wiki/FFI-%28interop-with-C%29 - printf doesn't work as expected #420
https://github.com/pixie-lang/pixie/issues/420 - ffi
:variadic? true
generated function do not use all arguments supplied #449
https://github.com/pixie-lang/pixie/issues/449 - Clojure home page
http://clojure.org/ - Clojure (downloads)
http://clojure.org/downloads - Clojure – Functional Programming for the JVM
http://java.ociweb.com/mark/clojure/article.html - Clojure quick reference
http://faustus.webatu.com/clj-quick-ref.html - 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 - Clojars:
https://clojars.org/ - Seznam knihoven na Clojars:
https://clojars.org/projects - 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 - Changes to Clojure in Version 1.8
https://github.com/clojure/clojure/blob/master/changes.md - Clojure core.async
http://www.infoq.com/presentations/clojure-core-async - core.async API Reference
https://clojure.github.io/core.async/ - Clojure core.async Channels
http://clojure.com/blog/2013/06/28/clojure-core-async-channels.html - core.async examples
https://github.com/clojure/core.async/blob/master/examples/walkthrough.clj - Timothy Baldridge – Core.Async
https://www.youtube.com/watch?v=enwIIGzhahw - List comprehension
http://en.wikipedia.org/wiki/List_comprehension - List Comprehensions in Clojure
http://asymmetrical-view.com/2008/11/18/list-comprehensions-in-clojure.html - Clojure Programming Concepts: List Comprehension
http://en.wikibooks.org/wiki/Clojure_Programming/Concepts#List_Comprehension