Programovací jazyk Julia: další stříbrná kulka v IT?

26. 5. 2016
Doba čtení: 14 minut

Sdílet

Po jazycích z pekla se budeme věnovat serióznějším věcem. Zajímavým příspěvkem na tomto poli je jazyk Julia, určený pro numerickou matematiku, zpracování signálů či statistiku.

Obsah

1. Programovací jazyk Julia: další stříbrná kulka v IT?

2. Interaktivní smyčka REPL

3. Další možnosti nabízené REPL

4. Aritmetické operátory, implicitní násobení

5. Funkce

6. Funkce vyššího řádu

7. Programové smyčky

8. Datové typy

9. Použití typů BigInt a BigFloat

10. Makra

11. Speciální funkce zobrazující generovaný nativní kód

12. Odkazy na Internetu

1. Programovací jazyk Julia: další stříbrná kulka v IT?

If you started from the beginning, you could recreate the things that people liked about those languages without so many of the problems.

Jedním z nejnovějších příspěvků do světa vyšších programovacích jazyků je jazyk nazvaný Julia, jehož první funkční verze byla zveřejněna před čtyřmi roky (i když práce na tomto jazyku začala již v roce 2009). Jedná se o programovací jazyk, který byl navržen takovým způsobem, aby dokázal nahradit takové nástroje, jakými jsou Matlab, R, GNU Octave či Python s knihovnami Numpy a SciPy a současně dokázal překládat programy do optimalizovaného kódu, který by mohl konkurovat Céčku či (dokonce) Fortranu. Důvod je jednoduchý – tvůrci jazyka Julia používali pro svou práci (numerická matematika a statistika) různé nástroje, z nichž každý byl specializován pro určitou oblast (Matlab pro výpočty s maticemi), R pro statistické výpočty apod.), ovšem kooperace mezi těmito nástroji nebyla ideální. Obecné jazyky typu Ruby či Python byly zase (opět podle mínění autorů jazyka Julia) pomalé, zejména při porovnání s klasickým céčkem, ale i s Javou.

Snahou původního autora Stefana Karpinského bylo vytvořit nový programovací jazyk založený na ve své době nejnovějších technologiích typu LLVM a současně používající to nejlepší ze světů imperativních i funkcionálních programovacích jazyků. Výsledkem měl být jazyk, který by byl snadno použitelný (zejména pro amatérské programátory), robustní, škálovatelný a současně i dostatečně rychlý, aby mohl soutěžit s C či Fortranem. Navíc měl jazyk Julia nabízet možnost snadné kooperace s knihovnami vytvořenými právě v C či Fortranu. Jestli se tento cíl skutečně podařilo splnit se pokusíme zjistit jak v tomto článku, tak i v několika navazujících článcích.

2. Interaktivní smyčka REPL

Podobně jako je tomu u mnoha dalších vyšších programovacích jazyků, je i Julia vybavena interaktivní smyčkou REPL (Read-Eval-Print-Loop), do níž může uživatel zadávat jednotlivé příkazy či deklarace, které se ihned předávají interpretru, který je zpracuje a popř. vypíše výsledek příkazu či chybovou zprávu. Smyčka REPL je vybavena pamětí dříve zapsaných příkazů a především pak možností automatického doplňování jmen funkcí atd. klávesou Tab. Nesmíme zapomenout ani na okamžité přepínání smyčky mezi několika režimy (zadávání příkazů, nápověda, shell atd.).

Po instalaci balíčku s programovacím jazykem Julia se interaktivní smyčka spustí příkazem julia. Na mém systému s Fedorou je dostupný poměrně starý interpret verze 0.3.9, ovšem na stránce http://julialang.org/downloads/ lze získat novější verze (nebojte se toho, že i nejnovější verze je pořád 0.x, ostatně ani tak vyspělý nástroj, jakým je Inkscape, taktéž ještě nedosáhl verze 1.x :-):

$ julia
               _
   _       _ _(_)_     |  A fresh approach to technical computing
  (_)     | (_) (_)    |  Documentation: http://docs.julialang.org
   _ _   _| |_  __ _   |  Type "help()" for help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 0.3.9
 _/ |\__'_|_|_|\__'_|  |
|__/                   |  x86_64-redhat-linux
 
julia>

Ve smyčce REPL funguje automatické doplňování příkazů, které se vyvolá klávesou Tab. Pokud smyčka nedokáže najít a doplnit příkaz jednoznačně (tj. když zadanému textu odpovídá více příkazů), vypíše všechny relevantní příkazy a očekává další akci uživatele. Zkusme si například vypsat všechny dostupné příkazy začínající na „a“:

julia> a
abs          accept       acot         acscd        airyai
airyprime    angle        applicable   asec         asind        atan2
abs2         acos         acotd        acsch        airyaiprime
airyx        any          apply        asecd        asinh        atand
abspath      acosd        acoth        addprocs     airybi
all          any!         apropos      asech        assert       atanh
abstract     acosh        acsc         airy         airybiprime
all!         append!      ascii        asin         atan         atexit

Ve chvíli, kdy uživatel stlačí klávesu „?“, přepne se smyčka REPL do režimu nápovědy, což je ihned viditelné změnou textu i barvy výzvy (promptu):

help?>

Můžeme si vyzkoušet získat nápovědu pro funkci quit, což je velmi snadné (opět lze použít automatické doplňování):

help?> quit
Base.quit()
 
   Quit the program indicating that the processes completed
   succesfully. This function calls "exit(0)" (see "exit()").
 
julia>

3. Další možnosti nabízené REPL

Kromě klávesy Tab, která slouží pro automatické doplňování jmen funkcí a dalších příkazů, je možné používat paměť již zadaných příkazů. Pro vyvolání příkazů z paměti smyčky REPL se používají klasické kurzorové šipky a taktéž klávesové zkratky Ctrl+S a Ctrl+R, jejichž význam je prakticky stejný, jako v shellu.

Zajímavá situace nastane ve chvíli, kdy na vstupním řádku zmáčkneme znak „;“ (středník). V tomto okamžiku se interaktivní smyčka REPL přepne do režimu shellu, v němž očekává stejné příkazy, jako v klasickém shellu (například v BASHi). To, že se režim smyčky REPL změní, poznáme snadno: změní se jak prompt (výzva), tak i barva promtpu na červenou:

shell>

Zkusme si nějaký příkaz spustit:

shell> whoami
tisnik
 
julia>

Užitečná je i funkce apropos(), která dokáže najít zadaný identifikátor ve všech dostupných modulech:

julia> apropos("quit")
INFO: Loading help data...
Base.quit()
Base.exit([code])
Base.Profile.clear_malloc_data()
Base.less(file::String[, line])
Base.less(function[, types])
Base.edit(file::String[, line])
Base.edit(function[, types])
julia> apropos("push")
Base.push!(collection, items...) -> collection
Base.pushdisplay(d::Display)
Base.Pkg.publish()
Base.Collections.heappush!(v, x[, ord])

Interpret si pamatuje poslední výsledek, který lze vyvolat pomocí automaticky naplňované proměnné nazvané ans (což možná někteří uživatelé znají z kalkulaček HP či z Matlabu):

julia> 6*8
48
 
julia> ans
48
 
julia> ans*ans
2304
 
julia> ans
2304

4. Aritmetické operátory, implicitní násobení

V jazyce Julia jsou (podle očekávání) dostupné všechny základní aritmetické operátory, tj. součet, rozdíl, součin, podíl, podíl modulo a umocnění. Pouze je zapotřebí si dát pozor na to, že pro dělení se používá operátor /, zatímco zdvojené lomítko // se používá pro zápis zlomků, což je jeden z datových typů tohoto jazyka:

julia> 1+2
3
 
julia> 6*7
42
 
julia> 10/3
3.3333333333333335
 
julia> 10//3
10//3
 
julia> 10%3
1
 
julia> 2^10
1024
 
julia> 1+2*3
7

Ve skutečnosti jsou všechny aritmetické operátory 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 jeden z významů bude vysvětlen v šesté kapitole v souvislosti s funkcemi vyššího řádu:

julia> +(1,2,3)
6
 
julia> *(1,2,3)
6

Užitečné je, že při násobení proměnné číselnou konstantou je možné vynechat operátor * a zapsat násobení tak, jak jsou uživatelé zvyklí z matematiky:

julia> x=10
10
 
julia> 2*x
20
 
julia> 2x
20
 
julia> 2*x+3*x^2
320
 
julia> 2x+3x^2
320

Pozor je zapotřebí dát na to, že tímto způsobem není možné násobit dvě proměnné – což je ostatně logické, neboť interpret neví, kde začíná jméno druhé proměnné:

julia> x=6
6
 
julia> y=7
7
 
julia> 2x
12
 
julia> 2x+3y
33
 
julia> x y
ERROR: syntax: extra token "y" after end of expression
 
julia> xy
ERROR: xy not defined

Některé konstanty a samozřejmě též uživatelem deklarované proměnné mohou ve svém jménu používat prakticky libovolné znaky Unicode. Příkladem je již dopředu deklarovaná konstanta π. V následujícím příkladu je tato konstanta vynásobena dvěma, což je možné, protože již v předchozím textu jsme se zmínili o možnosti vynechání znaku * při násobení číselnou konstantou:

julia> 
6.283185307179586

5. Funkce

Funkce se v programovacím jazyku Julia vytváří podobným způsobem, jaký známe například z jazyka Lua. První způsob vypadá následovně:

function add(x,y)
    return x+y
end
 
add (generic function with 1 method)
 
julia> add(1,2)
3

Druhý způsob je prakticky stejný, ovšem vynechá se příkaz return, který zde není nutný, neboť z funkce se vrací poslední vyhodnocovaný výraz:

function add(x,y)
    x+y
end
 
add (generic function with 1 method)
 
julia> add(1,2)
3

Poslední „jednořádkový“ způsob se použije ve chvíli, kdy je tělo funkce skutečně jednoduché. Tento způsob se nejvíce podobá matematickému zápisu funkcí:

julia> mul(x,y)=x*y
mul (generic function with 1 method)
 
julia> mul(6,7)
42

Pokud si chcete být jisti tím, jaká funkce se volá a s jakými typy parametrů, použijte makro @which:

julia> @which abs(42)
abs(x::Signed) at int.jl:75
 
julia> @which sin(42)
sin(x::Real) at math.jl:126

6. Funkce vyššího řádu

Programovací jazyk Julia sice není, na rozdíl od Haskellu a částečně i Clojure, čistě funkcionální jazyk, nicméně i zde hrají při vývoji aplikací i jednoduchých výpočtů velkou roli funkce vyššího řádu, tj. funkce, které jako své parametry akceptují jiné funkce popř. dokonce vrací (nové) funkce jako svoji návratovou hodnotu. Mezi dvě základní funkce vyššího řádu, patří funkce nazvané map a taktéž apply. Funkce map jako svůj první parametr akceptuje jinou funkci (s jedním parametrem) a druhým parametrem musí být seznam, n-tice, pole atd.. map postupně aplikuje předanou funkci na jednotlivé prvky seznamu a vytváří tak novou sekvenci. Podívejme se na dva příklady:

julia> inc(x)=x+1
inc (generic function with 1 method)
 
julia> inc(2)
3
 
julia> map(inc, range(1,10))
10-element Array{Int64,1}:
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
julia> square(x)=x*x
square (generic function with 1 method)
 
julia> map(square, range(1,10))
10-element Array{Int64,1}:
   1
   4
   9
  16
  25
  36
  49
  64
  81
 100

Funkce apply se chová poněkud odlišně – aplikuje totiž nějakou funkci (svůj první parametr) na předaný seznam, n-tici, pole atd. Typický „školní“ příklad s binární funkcí * (tj. funkcí se dvěma parametry) může vypadat následovně (jedná se o výpočet faktoriálu):

julia> apply(*, range(1,10))
3628800

7. Programové smyčky

V jazyku Julia lze použít všechny základní typy programových smyček. Podívejme se na způsob jejich zápisu. Povšimněte si, že se nepoužívají žádné dvojtečky (viz Python) ani klíčová slova typu do (Lua) či begin:

Smyčka typu while:

while výraz
    příkaz
    příkaz
    příkaz
end

Smyčka typu for-each:

for item in iter
    příkaz
    příkaz
    příkaz
end

Smyčka typu for má několik forem, z nichž nejpoužívanější je pravděpodobně forma, v níž se používá počitadlo, které postupně nabývá hodnoty ze zadaného rozsahu s případným krokem. Následující příklad vytiskne čísla 1 až 10 na jediný řádek:

for i=1:10
    print(i)
end
12345678910

Samozřejmě můžeme provést odřádkování po každém vypsaném číslu a současně provést nějaký výpočet (zde konkrétně počítáme mocninu dvou):

for i=1:10
    println(i,"\t",2^i)
end
 
1       2
2       4
3       8
4       16
5       32
6       64
7       128
8       256
9       512
10      1024

Podívejme se ještě na použití funkce range a * společně s funkcí vyššího řádu nazvanou apply() pro výpočet faktoriálu:

for i=1:10
    println(i,"\t",apply(*, range(1,i)))
end
 
1       1
2       2
3       6
4       24
5       120
6       720
7       5040
8       40320
9       362880
10      3628800

Smyčka typu for s větším množstvím řídicích proměnných:

for i=..., j=...
    příkaz
    příkaz
    příkaz
end

Podrobnosti o použití těchto smyček ve složitějších či rozsáhlejších algoritmech si řekneme v následujícím článku.

8. Datové typy

Mezi základní datové typy, s nimiž je možné v programovacím jazyku Julia pracovat, patří především Bool, Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float16, Float32, Float64, Complex64 a Complex128. U každé hodnoty popř. proměnné lze typ velmi snadno zjistit, a to v režimu nápovědy. Příkladem může být zjištění typu konstanty 42 – nejprve stlačíme „?“ pro přepnutí do režimu nápovědy a následně napíšeme 42 + Enter. Výsledek by měl vypadat zhruba následovně:

help?> 42
42 is of type
DataType   : Int64
  supertype: Signed

Povšimněte si použití proměnné ans, o níž jsme se již zmiňovali v předchozích kapitolách:

julia> 1/3
0.3333333333333333
 
help?> ans
0.3333333333333333 is of type
DataType   : Float64
  supertype: FloatingPoint

Zlomky zapisované symbolem // mají svůj vlastní datový typ:

julia> 1//3
1//3
 
help?> ans
1//3 is of type
DataType   : Rational{Int64} (constructor with 1 method)
  supertype: Real
  fields   : (:num,:den)

Pracovat je možné i s hodnotami nekonečno (Inf) a „nečíslo“ (NaN):

help?> Inf
Inf is of type
DataType   : Float64
  supertype: FloatingPoint
 
help?> NaN
NaN is of type
DataType   : Float64
  supertype: FloatingPoint

U těchto hodnot je samozřejmě definováno jejich přesné chování u většiny aritmetických operací i u většiny funkcí:

julia> 1/0
Inf
 
julia> Inf-Inf
NaN
 
julia> 1/Inf
0.0

Dalším datovým typem je komplexní číslo, které se vytváří jednoduše – použije se konstanta im, kterou se vynásobí hodnota, která se má převést na imaginární složku:

help?> im
im is of type
DataType   : Complex{Bool} (constructor with 1 method)
  supertype: Number
  fields   : (:re,:im)

Jedna z možných konstrukcí komplexního čísla může vypadat takto:

julia> 1+2im
1 + 2im
 
help?> ans
1 + 2im is of type
DataType   : Complex{Int64} (constructor with 1 method)
  supertype: Number
  fields   : (:re,:im)

Samozřejmě budou pracovat i obvyklé operace s komplexními čísly:

julia> z=1+2im
1 + 2im
 
julia> z^2
-3 + 4im

Složenými datovými typy, poli atd. se budeme zabývat příště.

9. Použití typů BigInt a BigFloat

Zkusme provést zdánlivě jednoduchý výpočet:

julia> 2^70
0

Výsledkem je překvapivě nula, což je samozřejmě špatný výsledek. Je tomu tak z toho důvodu, že konstanty 2 i 70 jsou typu integer (konkrétně 64bitové celé číslo se znaménkem), tudíž i výpočet se provádí s typem integer:

help?> 2
2 is of type
DataType   : Int64
  supertype: Signed

Můžeme však jednu z konstant převést na BigInt, což jsou celá čísla s prakticky neomezeným rozsahem. Nyní již výpočet proběhne bez chyby:

julia> BigInt(2)^70
1180591620717411303424

Výpočet faktoriálu 100! nyní podle očekávání taktéž nedopadne správně:

julia> apply(*, range(1,100))
0

Po malé úpravě je již vše v pořádku:

julia> apply(*, range(BigInt(1),100))
933262154439441526816992388562667004907159682643816214685929638952175
999932299156089414639761565182862536979208272237582511852109168640000
00000000000000000000

Namísto konstruktoru BigInt lze použít i funkci big(), která podle typu vstupní hodnoty vytvoří BigInt či BigFloat:

julia> apply(*, range(big(1),100))
933262154439441526816992388562667004907159682643816214685929638952175
999932299156089414639761565182862536979208272237582511852109168640000
00000000000000000000

Funkce big() nám pomůže i při výpočtu hodnot s nekonečným rozvojem:

julia> 1/33
0.030303030303030304
 
julia> big(1)/33
3.030303030303030303030303030303030303030303030303030303030303030303030303030301e-02 with 256 bits of precision
 
julia> big(1)/7
1.428571428571428571428571428571428571428571428571428571428571428571428571428568e-01 with 256 bits of precision
 
julia> pi
π = 3.1415926535897...
 
julia> big(pi)
3.141592653589793238462643383279502884197169399375105820974944592307816406286198e+00 with 256 bits of precision

10. Makra

V programovacím jazyku Julia je možné vytvářet i poměrně složitá makra, která se podobají makrům známým například z různých variant LISPu či z programovacího jazyka Clojure. V dnešním článku nemáme dostatek prostoru pro vysvětlení, jakým způsobem se makra zapisují a jak se vyhodnocují, ale ukážeme si některé možnosti jejich použití. Proč se vlastně makra používají a k čemu jsou užitečná? Makra jsou spouštěna ve chvíli, kdy dochází ke zpracování vstupního zdrojového kódu, což znamená, že se makra dají použít pro úpravu tohoto kódu (manipulaci s kódem) ještě předtím, než dojde k jeho interpretaci či překladu. Parametry maker tedy ještě nejsou vyhodnoceny, na rozdíl od parametrů předávaných funkcím. To mj. znamená, že v makrech lze implementovat smyčky či podmínky (podmínka typu if nelze zapsat běžnou funkcí, protože by došlo k vyhodnocení všech parametrů).

Podívejme se nyní na některá užitečná makra. To, že se volá makro, se dá poznat z použití znaku @ před jménem makra. Příkladem může být @which, které nalezne a vypíše konkrétní funkci, která se pro daný výraz zavolá:

julia> @which abs(42)
abs(x::Signed) at int.jl:75
 
julia> @which sin(x^2)
sin(x::Real) at math.jl:126

Pokud budeme potřebovat zjistit, jak dlouho trvá nějaký výpočet, lze použít makro nazvané @time, které vypíše čas běhu výpočtu a alokovanou paměť. Výsledkem je však přímo výsledek výpočtu, tj. okolní kód „nepozná“, že se volalo makro:

julia> sin(2pi)
-2.4492935982947064e-16
 
julia> @time sin(2pi)
elapsed time: 9.708e-6 seconds (112 bytes allocated)
-2.4492935982947064e-16
 
julia> @time apply(*,range(big(1),100))
elapsed time: 0.000279437 seconds (44632 bytes allocated)
933262154439441526816992388562667004907159682643816214685929
638952175999932299156089414639761565182862536979208272237582
51185210916864000000000000000000000000

Podobně se chová makro nazvané @timed, které ovšem vrací odlišný výsledek, konkrétně n-tici obsahující jak výsledek výpočtu, tak i čas běhu a alokovanou paměť v bajtech. To znamená, že tyto hodnoty je možné programově zpracovat:

julia> @timed sin(x^2)
(-0.9917788534431158,7.962e-6,96,0.0)
 
julia> @timed apply(*,range(big(1),100))
(93326215443944152681699238856266700490715968264381621468592
963895217599993229915608941463976156518286253697920827223758
251185210916864000000000000000000000000,0.000265888,44632,0.0)

Užitečným makrem při ladění je makro nazvané @show, které zobrazí vyhodnocovaný výraz a současně vrátí i jeho výsledek:

julia> @show sin(2^x)/cos(2)
sin(2^x) / cos(2) => -2.2108206945184055
-2.2108206945184055

11. Speciální funkce zobrazující generovaný nativní kód

V úvodní kapitole jsme si řekli, že jazyk Julia využívá on-the-fly překlad do nativního kódu, a to s využitím technologie LLVM. Pomocí několika maker se můžeme podívat, jak přibližně takový překlad může vypadat. Dnes prozatím jen velmi stručně:

ict ve školství 24

julia> @code_llvm sin(x^2)
 
define double @julia_sin_20564(i64) {
top:
  %1 = sitofp i64 %0 to double, !dbg !8
  %2 = call double inttoptr (i64 140194494233504 to double (double)*)(double %1), !dbg !8
  %3 = fcmp uno double %1, 0.000000e+00, !dbg !8
  %4 = fcmp ord double %2, 0.000000e+00, !dbg !8
  %5 = or i1 %4, %3, !dbg !8
  br i1 %5, label %pass, label %fail, !dbg !8
 
fail:                                             ; preds = %top
  %6 = load %jl_value_t** @jl_domain_exception, align 8, !dbg !8, !tbaa %jtbaa_const
  call void @jl_throw_with_superfluous_argument(%jl_value_t* %6, i32 126), !dbg !8
  unreachable, !dbg !8
 
pass:                                             ; preds = %top
  ret double %2, !dbg !8
}

Průběh překladu do nativního kódu (zde používám stroj s procesorem řady x86_64):

julia> @code_native sin(x^2)
        .text
Filename: math.jl
Source line: 0
        push    rbp
        mov     rbp, rsp
Source line: 126
        sub     rsp, 16
        cvtsi2sd        xmm0, rdi
        movsd   qword ptr [rbp - 8], xmm0
        movabs  rax, 140194494233504
        call    rax
        ucomisd xmm0, xmm0
        jp      6
        add     rsp, 16
        pop     rbp
        ret
        movsd   xmm1, qword ptr [rbp - 8]
        ucomisd xmm1, xmm1
        jp      -17
        movabs  rax, 140194893713480
        mov     rax, qword ptr [rax]
        movabs  rcx, 140194891018528
        mov     esi, 126
        mov     rdi, rax
        call    rcx

(jedná se jen o ukázku možností prostředí jazyka Julia, nikoli o podrobný popis, jak přesně se provádí překlad zdrojový kód→AST→LLVM→nativní kód).

12. Odkazy na Internetu

  1. Julia (front page)
    http://julialang.org/
  2. Julia – dokumentace
    http://docs.julialang.org/en/release-0.4/
  3. Julia – repositář na GitHubu
    https://github.com/JuliaLang/julia
  4. Julia (programming language)
    https://en.wikipedia.org/wi­ki/Julia_%28programming_lan­guage%29
  5. IJulia
    https://github.com/JuliaLan­g/IJulia.jl
  6. Introducing Julia
    https://en.wikibooks.org/wi­ki/Introducing_Julia
  7. Julia: the REPL
    https://en.wikibooks.org/wi­ki/Introducing_Julia/The_REPL
  8. Introducing Julia/Metaprogramming
    https://en.wikibooks.org/wi­ki/Introducing_Julia/Meta­programming
  9. Month of Julia
    https://github.com/DataWo­okie/MonthOfJulia
  10. Learn X in Y minutes (where X=Julia)
    https://learnxinyminutes.com/doc­s/julia/
  11. New Julia language seeks to be the C for scientists
    http://www.infoworld.com/ar­ticle/2616709/application-development/new-julia-language-seeks-to-be-the-c-for-scientists.html
  12. Julia: A Fast Dynamic Language for Technical Computing
    http://karpinski.org/publi­cations/2012/julia-a-fast-dynamic-language
  13. The LLVM Compiler Infrastructure
    http://llvm.org/
  14. Julia: benchmarks
    http://julialang.org/benchmarks/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.