Obsah
1. Programovací jazyk Julia: funkce jako základní stavební prvek programů
5. Numerický výpočet určitého integrálu
6. Funkce s větším množstvím návratových hodnot
7. Funkce s proměnným počtem argumentů (variadické funkce)
8. Funkce s volitelnými argumenty
9. Funkce s pojmenovanými argumenty (keyword arguments)
10. Operátory jsou jen jinak zapisované funkce
11. Jména funkcí pro speciální operátory
12. Funkce vyššího řádu: map a pmap
13. Další užitečné funkce vyššího řádu reduce, foldl a foldr
14. Speciální varianty reduce: maximum, minimum a sum
1. Programovací jazyk Julia: funkce jako základní stavební prvek programů
Základní informace o použití funkcí v programovacím jazyku Julia jsme si již řekli v první i ve druhé části tohoto seriálu, ovšem vzhledem k tomu, že funkce (a taktéž operátory, což jsou ve skutečnosti jen odlišně zapisované funkce) mají v tomto jazyku význam základního stavebního prvku, se jimi dnes budeme zabývat podrobněji. Nezapomeneme samozřejmě ani na použití anonymních funkcí, především v souvislosti s funkcemi vyššího řádu. Programovací jazyk Julia totiž přebírá mnoho svých vlastností z klasických funkcionálních jazyků, takže v něm nalezneme jak základní funkce vyššího řádu typu map (i její paralelní verze pmap), apply, reduce (specifikované přesněji ve dvojici foldl a foldr), tak i různé další funkce, které jako svůj parametr akceptují jiné funkce. Příkladem může být funkce nazvaná quadgk určená pro numerickou integraci libovolné funkce, která je do quadgk předána.
2. Běžné funkce
Běžný zápis funkce vypadá velmi jednoduše a do značné míry se podobá zápisu funkce v programovacím jazyku Lua: definice funkce začíná klíčovým slovem function, za nímž následuje jméno funkce se seznamem parametrů umístěných v závorce. Po této hlavičce je možné zapsat libovolné množství příkazů tvořících tělo funkce, přičemž na libovolném místě lze v případě potřeby použít příkaz return pro ukončení funkce a pro vrácení návratové hodnoty či hodnot. Celá definice funkce končí klíčovým slovem end. Typickým příkladem je uživatelská funkce určená pro součin dvou hodnot:
function mul(x,y) return x*y; end
Ve skutečnosti není středník důležitý, takže ho lze vynechat:
function mul(x,y) return x*y end
V tomto případě je možné klíčové slovo return vynechat, protože hodnota posledního příkazu či výrazu se automaticky považuje i za návratovou hodnotu funkce:
function mul(x,y) x*y; end
Opět je možné vynechat přebytečný středník:
function mul(x,y) x*y end
Z předchozí části tohoto seriálu ovšem již víme, že se funkce mohou skládat z metod, přičemž se jednotlivé metody od sebe odlišují typem parametrů. To mj. znamená, že funkci mul (resp. jednu její metodu) můžeme napsat i takto:
function mul(x::Number, y::Number) x*y end
Navíc můžeme k výše zapsané metodě akceptující jako své parametry dvojici čísel libovolného typu přidat i metodu určenou pro výpočet skalárního součinu dvou vektorů (opět pro zjednodušení vynecháme klíčové slovo return):
function mul(x::Array{Int,1}, y::Array{Int,1}) vecdot(x,y) end
Poznámka: zápis Array{Int,1} popisuje pole s jednou dimenzí (tedy vektor) s prvky typu Int. Ovšem samotný typ Int je ve skutečnosti abstraktním typem a může značit konkrétní typ Int8, Int16, Int32 atd. až Int128.
Obě dvě metody tvořící dohromady funkci mul si můžeme velmi snadno otestovat:
julia> mul(6,7) 42 julia> mul([1,2], [1,2]) 5 julia> mul([1,2,3], [1,2,3]) 14 julia> mul([1,2,3], [1,2]) ERROR: DimensionMismatch("vector x has length 3, but vector y has length 2") in vecdot at linalg/generic.jl:248 in mul at none:2
V posledním kroku vidíme, že se nepodařilo vynásobit dva vektory s různou délkou (což je pochopitelná vlastnost).
3. Zkrácený zápis funkce
Kromě definice uživatelských funkcí, s nímž jsme se seznámili v předchozí kapitole, podporuje programovací jazyk Julia i alternativní „jednořádkový“ způsob zápisu. Tento způsob se typicky používá ve chvíli, kdy je tělo funkce skutečně jednoduché, protože pro delší či složitější funkce se zápis na jediném řádku stává dosti nepřehledným. Tento způsob se nejvíce podobá matematickému zápisu funkcí a je pro něj typické vynechání klíčových slov function a end:
julia> f(x,y) = x+y f (generic function with 1 method)
Naši demonstrační funkci pro vynásobení dvou hodnot lze přepsat i takto:
julia> mul(x,y)=x*y mul (generic function with 1 method) julia> mul(6,7) 42
U tohoto zápisu se můžeme často setkat s použitím ternárního operátoru:
julia> max2(x,y) = x>y ? x : y max2 (generic function with 1 method) julia> max2(1,2) 2 julia> max2(2,1) 2
Zápis s použitím konstrukce if-then-else vypadá takto:
julia> max2(x,y) = if x > y x else y end max2 (generic function with 1 method) julia> max2(1,2) 2 julia> max2(2,1) 2
4. Anonymní funkce
Programovací jazyk Julia podporuje i tvorbu takzvaných anonymních funkcí, což jsou funkce (programový kód, který lze zavolat), kterým není přiřazeno jméno. Předností takových funkcí je především sevřený styl zápisu, v němž se nepoužívají klíčová slova function a end, ale (alespoň většinou) výraz či výrazy představující nějaký výpočet na základě parametrů předaných anonymní funkci. Definici anonymní funkce tak lze použít například i jako parametr jiné funkce (funkce vyššího řádu). Při definici anonymních funkcí se používá dvojznak ->, před nějž se zapisuje parametr či parametry a za něj výraz, jehož vypočtený výsledek je současně návratovou hodnotou anonymní funkce. Nejprve se podívejme na jednoduchou anonymní funkci s jediným parametrem:
julia> x -> x+1 (anonymous function)
Funkce se dvěma parametry vypadá takto:
julia> (x,y) -> x+y (anonymous function)
Podobným způsobem lze samozřejmě definovat i funkce s vyšším počtem parametrů.
5. Numerický výpočet určitého integrálu
Podívejme se nyní na praktický příklad použití anonymních funkcí, a to konkrétně při výpočtu určitého integrálu. Pro podobné výpočty sice existují specializované knihovny (z nichž naprostou většinou lze z jazyka Julia volat), ovšem pro jednodušší účely může postačit použít funkci nazvanou quadgk(), která je dostupná i v případě, že použijeme pouze základní balíčky (knihovny) programovacího jazyka Julia. Podívejme se, jak lze vypočítat určitý integrál goniometrické funkce sinus pro meze 0 až π. Funkce quadgk() je funkce vyššího řádu, protože jako svůj první parametr akceptuje jinou funkci, konkrétně funkci, jejíž faktoriál budeme počítat:
julia> quadgk(sin, 0, pi) (2.0,1.7905676941154525e-12)
Výsledkem je hodnota určitého integrálu (plocha pod křivkou) a předpokládaná absolutní chyba.
Pokud budeme chtít použít uživatelské funkce, máme dvě možnosti. Buď tyto funkce definovat a pojmenovat (viz druhou kapitolu), což si vyžádá určitý prostor ve zdrojovém kódu, nebo můžeme použít funkce anonymní. Vypočtěme například plochu obdélníku 10×1 jednotka (anonymní funkce ignoruje svůj parametr):
julia> quadgk(x->1, 0, 10) (10.0,0.0)
Plocha pod křivkou y=x pro různé meze:
julia> quadgk(x->x, 0, 1) (0.5,0.0) julia> quadgk(x->x, -10, 10) (0.0,0.0)
Plocha pod křivkou y=x x:
julia> quadgk(x->x^x, 0, 1) (0.7834305106229384,4.864047851993458e-9)
atd.
6. Funkce s větším množstvím návratových hodnot
Někteří čtenáři si možná už povšimli, že se programovací jazyk Julia v mnoha ohledech podobá jazyku Lua (což není náhoda). Týká se to i další užitečné vlastnosti – funkce totiž nemusí vracet jen jednu hodnotu, ale (prakticky) libovolný počet hodnot. Pokud potřebujete vrátit například dvě hodnoty, lze za klíčové slovo return zadat dva výrazy, které jsou od sebe oddělené čárkou. Příkladem může být funkce swap() akceptující dva parametry a vracející vyhodnocené parametry v opačném pořadí:
julia> function swap(x,y) return y,x end swap (generic function with 1 method)
Při volání této funkce v interaktivním vývojovém prostředí ihned obě vrácené hodnoty uvidíme na obrazovce (budou zobrazeny v kulatých závorkách):
julia> swap(1,2) (2,1)
Příkladem může být například uživatelská funkce nazvaná minmax(), která pro předané pole nalezne jeho minimální a maximální prvek. Oba tyto prvky vrátí volajícímu programovému kódu:
julia> function minmax(array) return minimum(array), maximum(array) end minmax (generic function with 1 method)
Vyzkoušení této funkce je snadné a přímočaré:
julia> minmax([1,2,3,4,3,2,1]) (1,4)
Poznámka: i výše popsaná funkce quadgk() určená pro numerickou integraci vracela dvě hodnoty: výsledek vlastního výpočtu (plochu pod křivkou) a absolutní chybu (resp. přesněji řečeno odhad chyby).
7. Funkce s proměnným počtem argumentů (variadické funkce)
V některých případech může být užitečné použít funkci či funkce s proměnným počtem argumentů. Tyto funkce se někdy nazývají variadické funkce a poznáme je podle toho, že se v seznamu parametrů v definici funkce nachází jméno argumentu následované třemi tečkami. Všechny další parametry se předají ve formě sekvence, kterou je samozřejmě možné zpracovat, přičemž je nutné mít na paměti, že tato sekvence může být i prázdná. Podívejme se na jednoduchý příklad – funkci pro výpočet sumy všech svých parametrů:
function suma(x...) local result = 0 for item in x result = result + item end result end
Funkci si ihned otestujme, a to i pro volání bez parametrů:
julia> suma() 0 julia> suma(1) 1 julia> suma(1,2) 3 julia> suma(1,2,3) 6 julia> suma(1,2,3,4, 1//2) 21//2
Nepovinné parametry lze zkombinovat s parametry povinnými, pouze je zapotřebí dodržet pravidlo, že nepovinné parametry se uvádí až za všemi parametry povinnými:
function scaled_suma(scale, x...) local result = 0 for item in x result = result + item end scale*result end
Nyní je první parametr povinný, o čemž se lze snadno přesvědčit:
julia> scaled_suma() ERROR: MethodError: `scaled_suma` has no method matching scaled_suma() julia> scaled_suma(1) 0 julia> scaled_suma(2, 1) 2 julia> scaled_suma(2, 1, 2, 3) 12
8. Funkce s volitelnými argumenty
Programovací jazyk Julia podporuje i práci s volitelnými argumenty. Pokud nejsou tyto argumenty naplněny při volání funkce, použije se jejich implicitní hodnota zadaná již v definici funkce. Díky podpoře funkcí s volitelnými argumenty tak nemusíme vytvářet metody se stejným jménem, ale s proměnným počtem parametrů (což je sice ekvivalentní, ale mnohdy zbytečně složité). Samozřejmě si opět ukážeme demonstrační příklad:
julia> function add(x, y=0, z=0) x+y+z end add (generic function with 3 methods) julia> add() ERROR: MethodError: `add` has no method matching add() julia> add(1) 1 julia> add(1,2) 3 julia> add(1,2,3) 6
9. Funkce s pojmenovanými argumenty (keyword arguments)
Poslední variantou funkcí jsou funkce s pojmenovanými argumenty. Aby interpret tyto argumenty korektně rozeznal, musíme pro jejich oddělení od běžných argumentů použít středník a nikoli pouze čárku:
function area(message; width=0, height=0) print(message) width*height end
Povšimněte si, že v tomto případě je nutné uvést výchozí hodnotu argumentů.
Při volání takové funkce se explicitně uvedou jména pojmenovaných argumentů:
julia> area("Plocha obdelniku ") Plocha obdelniku 0 julia> area("Plocha ctverce ", width=2, height=2) Plocha ctverce 4 julia> area("Plocha obdelniku ", width=10, height=1) Plocha obdelniku 10
Díky podpoře pro pojmenované parametry lze vytvářet čitelné volání funkcí, typicky funkcí pro vykreslování grafů (specifikace stylu vykreslování) apod.
10. Operátory jsou jen jinak zapisované funkce
Již v úvodním článku jsme si řekli, že ve skutečnosti jsou všechny aritmetické operátory (součet, rozdíl, součin, podíl atd.) implementovány běžnými funkcemi pojmenovanými stejným znakem, jaký odpovídá danému operátoru. To například znamená, že součet dvou čísel lze zapsat jako 1+2, ale také formou funkce +(1,2), což je sice možná poněkud podivné, ale ve skutečnosti užitečné, protože takto zapsaný operátor má volitelnou aritu (počet operandů). Ovšem současně to také znamená, že můžeme například definovat metodu + pro nový uživatelsky definovaný datový typ a tak vlastně operátor „přetížit“ (což je chování, které známe prakticky ze všech programovacích jazyků, i když u některých z nich je přetížení součástí standardu jazyka a není přístupné uživatelům). Aby bylo chování konzistentní, je nutné takovou metodu vytvořit s variadickým počtem parametrů.
11. Jména funkcí pro speciální operátory
Kromě jmen základních funkcí +, -, *, /, % a ^ představujících aritmetické operátory se můžeme setkat s následujícími funkcemi, které syntakticky nahrazují své operátory (to tedy znamená, že následující operátory nelze použít jako přímé jméno funkce, protože by to syntakticky nedávalo smysl):
Zápis | Volá se funkce/metoda |
---|---|
[A B C …] | hcat() |
[A, B, C, …] | vcat() |
[A B; C D; …] | hvcat() |
A' | ctranspose() |
A.' | transpose() |
1:n | colon() |
A[i] | getindex() |
A[i]=x | setindex!() |
A(x) | call() |
Pokud tedy potřebujete přetížit příslušný operátor, je nutné ve skutečnosti definovat příslušnou metodu hcat, vcat, call atd.
12. Funkce vyššího řádu: map a pmap
Zajímavé a užitečné je použití takzvaných funkcí vyššího řádu. Jedná se o funkce, které jako svůj parametr (parametry) akceptují jiné funkce popř. nějaké funkce vrací. My jsme se již s jednou podobnou funkcí setkali – jednalo se o knihovní funkci quadgk() určenou pro numerickou integraci. Ovšem ve standardní knihovně nalezneme i další užitečné funkce vyššího řádu. Programátoři používající nějaký funkcionální jazyk pravděpodobně znají funkci map určenou pro aplikaci nějaké jiné funkce na prvky sekvence, seznamu či pole. Tuto funkci vyššího řádu pochopitelně nalezneme i v programovacím jazyku Julia, takže se podívejme na možnosti jejího použití.
Definujme uživatelskou funkci pro výpočet binomických koeficientů (kombinačních čísel) „n nad k“:
function binom(n,k) n >= k || return 0 n == 1 && return 1 k == 0 && return 1 (n * binom(n - 1, k - 1)) ÷ k end
Pokud nyní budeme chtít vypočítat šestý řádek Pascalova trojúhelníku, není nic snadnějšího:
julia> map(x->binom(5,x), 0:5) 6-element Array{Int64,1}: 1 5 10 10 5 1
Podobně můžeme vypočítat desátý řádek Pascalova trojúhelníku:
julia> map(x->binom(10,x) ,0:10) 11-element Array{Int64,1}: 1 10 45 120 210 252 210 120 45 10 1
V jazyku Julia nalezneme i paralelní variantu funkce map, která se jmenuje pmap. Aby bylo možné tuto variantu efektivně využít, je nutné společně s interpretrem spustit i větší množství tzv. workerů, a to zadáním parametru -p n, kde n je počet workerů (nastavit lze například na počet jader). Dále je nutné, aby funkce, která se má počítat paralelně, byla dostupná všem workerům, což zajistí makro @everywhere. Vyzkoušejme si například paralelní výpočet Ackermannovy funkce, protože vyhodnocení této funkce je i přes její jednoduchý zápis poměrně zdlouhavé (a taktéž náročné na kapacitu zásobníku):
@everywhere(function ackermann(m,n) if m == 0 return n + 1 elseif n == 0 return ack(m-1,1) else return ack(m-1,ack(m,n-1)) end end)
Sériový výpočet hodnot Ackermannovy funkce pro parametry [4,1], [4,2], [4,3] a [4,4] bude vypadat takto:
map(x->ackermann(4,x), 1:4)

Obrázek 1: Sériový výpočet hodnot Ackermannovy funkce. Procesor se „fláká“ na pouhé čtvrtině jeho skutečného výkonu.
Po spuštění interpretru a čtyř workerů příkazem…
julia -p 4
…je již možné použít paralelní verzi funkce map:
pmap(x->ackermann(4,x), 1:4)

Obrázek 2: Paralelní výpočet hodnot Ackermannovy funkce. Povšimněte si paralelně pracujících workerů a vytížení procesoru.
13. Další užitečné funkce vyššího řádu reduce, foldl a foldr
Další známou funkcí vyššího řádu, kterou nalezneme v prakticky každém funkcionálním jazyce, je funkce nazvaná reduce, která slouží pro postupné zpracování vstupní sekvence po jednotlivých prvcích. Pokud použijeme binární operátor, aplikuje se na první dva prvky v sekvenci, následně se aplikuje na výsledek této operace společně se třetím prvkem atd. Určitým problémem je, že u funkce reduce není předepsáno, zda se sekvence bude zpracovávat od začátku či od konce, takže se v programovacím jazyku Julia vyskytují další dvě funkce vyššího řádu nazvané foldl a foldr, u nichž je směr zpracování sekvence pevně zadaný (což ale může znamenat určité zpomalení). U některých operací směr zpracování nehraje žádnou roli (komutativita):
julia> reduce(*, 1:10) 3628800 julia> foldl(*, 1:10) 3628800 julia> foldr(*, 1:10) 3628800
Ovšem u dalších operací se již výsledky mohou lišit:
julia> reduce(/, [1,2,3]) 0.16666666666666666 julia> foldl(/, [1,2,3]) 0.16666666666666666 julia> foldr(/, [1,2,3]) 1.5
Zde mimochodem můžeme vidět, že v tomto konkrétním případě se funkce reduce chová stejně jako funkce foldl, tj. „redukuje“ vstupní sekvenci od začátku směrem ke konci.
14. Speciální varianty reduce: maximum, minimum a sum
Výše popsanou funkci vyššího řádu reduce, popř. její konkrétnější varianty foldl a foldl, je možné použít pro implementaci mnoha dalších užitečných funkcí. Příkladem může být funkce pro výpočet maximálního prvku v poli. Tuto funkci lze definovat následovně:
function maximum_impl(array) reduce((x,y) -> x>y ? x:y, array) end maximum_impl([4,3,2,1]) 4
Vidíme zde použití anonymní funkce akceptující dva parametry a vracející větší z těchto parametrů (je zde použit ternární operátor, který umožňuje tělo anonymní funkce zapsat jediným výrazem).
Ovšem vzhledem k tomu, že podobné funkce potřebujeme v programech používat velmi často, obsahuje již základní knihovna jazyka Julia jejich optimalizované verze. Podívejme se na demonstrační příklady – budeme počítat maximální a minimální hodnotu pole s výsledkem výpočtu binomických koeficientů „deset nad k“. Navíc ještě spočítáme součet všech těchto koeficientů:
function binom(n,k) n >= k || return 0 n == 1 && return 1 k == 0 && return 1 (n * binom(n - 1, k - 1)) ÷ k end b=map(x->binom(10,x),0:10) 11-element Array{Int64,1}: 1 10 45 120 210 252 210 120 45 10 1
Pole s vypočtenými prvky máme připraveno, takže ho ihned můžeme použít:
julia> minimum(b) 1 julia> maximum(b) 252 julia> sum(b) 1024
Poznámka: kromě výše uvedených funkcí minimum(), maximum() a sum() nalezneme ve standardní knihovně programovacího jazyka Julia i další užitečné funkce pracující s celým polem či sekvencí hodnot. Příkladem může být funkce prod() pracující podobně jako sum(), ovšem prvky předaného pole jsou vynásobeny a nikoli sečteny:
julia> prod([1,2,3]) 6 julia> prod(1:10) 3628800
15. Predikáty any a all
Do stejné kategorie, jako výše zmíněné funkce, patří i takzvané predikáty, tj. funkce vracející pravdivostní hodnotu true nebo false na základě předaných parametrů. Zajímat nás nyní budou dva predikáty nazvané any a all. Predikát any vrací pravdivostní hodnotu true ve chvíli, kdy alespoň jeden prvek pole (či sekvence) splňuje zapsanou podmínku. Tuto podmínku je možné při volání predikátu zapsat například formou anonymní funkce, takže celé vyhodnocení predikátu je možné provést na jediném řádku ve zdrojovém kódu. Nejprve si připravme pole, jehož prvky budeme zkoumat:
julia> b=map(x->binom(10,x), 0:10) 11-element Array{Int64,1}: 1 10 45 120 210 252 210 120 45 10 1
Nyní již můžeme zjistit informace o celém poli, například existenci prvku většího než 200 či naopak alespoň jednoho záporného prvku:
julia> any(x -> x>200, b) true julia> any(x -> x<0, b) false
Naproti tomu predikát nazvaný all vrací pravdivostní hodnotu true pouze tehdy, když je zadaná podmínka platná pro všechny prvky pole či sekvence. V opačném případě, tedy pokud alespoň jeden prvek podmínku nesplňuje, pochopitelně vrací false:
julia> all(x -> x>200, b) false julia> all(x -> x>0, b) true
16. Odkazy na Internetu
- Introducing Julia/Functions
https://en.wikibooks.org/wiki/Introducing_Julia/Functions - Functions (Julia documentation)
http://docs.julialang.org/en/release-0.4/manual/functions/ - Evaluate binomial coefficients
http://rosettacode.org/wiki/Evaluate_binomial_coefficients - Ackermann function
http://rosettacode.org/wiki/Ackermann_function - Julia (front page)
http://julialang.org/ - Julia – dokumentace
http://docs.julialang.org/en/release-0.4/ - Julia – repositář na GitHubu
https://github.com/JuliaLang/julia - Julia (programming language)
https://en.wikipedia.org/wiki/Julia_%28programming_language%29 - IJulia
https://github.com/JuliaLang/IJulia.jl - Introducing Julia
https://en.wikibooks.org/wiki/Introducing_Julia - Julia: the REPL
https://en.wikibooks.org/wiki/Introducing_Julia/The_REPL - Introducing Julia/Metaprogramming
https://en.wikibooks.org/wiki/Introducing_Julia/Metaprogramming - Month of Julia
https://github.com/DataWookie/MonthOfJulia - Learn X in Y minutes (where X=Julia)
https://learnxinyminutes.com/docs/julia/ - New Julia language seeks to be the C for scientists
http://www.infoworld.com/article/2616709/application-development/new-julia-language-seeks-to-be-the-c-for-scientists.html - Julia: A Fast Dynamic Language for Technical Computing
http://karpinski.org/publications/2012/julia-a-fast-dynamic-language - The LLVM Compiler Infrastructure
http://llvm.org/ - Julia: benchmarks
http://julialang.org/benchmarks/ - Type system
https://en.wikipedia.org/wiki/Type_system - Half-precision floating-point format
https://en.wikipedia.org/wiki/Half-precision_floating-point_format