Jdi na navigaci předmětu

03: Řídící struktury a Typový systém

Tento notebook je výukovým materiálem v předmětu BI-JUL.21 vyučovaném v zimním semestru akademického roku 2025/2026 Tomášem Kalvodou. Tvorba těchto materiálů byla podpořena NVS FIT.

Hlavní stránkou předmětu, kde jsou i další notebooky a zajímavé informace, je jeho Course Pages stránka.

versioninfo()
Julia Version 1.13.0
Commit d1c37793dd2 (2026-09-09 19:00 UTC)
Build Info:
  Official https://julialang.org release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
  WORD_SIZE: 64
  LLVM: libLLVM-20.1.8 (ORCJIT, skylake)
  GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 8 virtual cores)

0. Úvodní poznámky (a.k.a. Rozcvička)

Než se pustíme do hlavních témat této lekce, tak si ukážeme základní prvky jazyka Julia (zejména co se syntaxe týče). K většině těchto témat se pak vrátíme podrobněji později během semestru. Nyní potřebujeme čtenáře zasvětit alespoň do úplných základů, jinak by nebyl další výklad tak zábavný.

Předpokládáme, že studenti již mají z předchozí lekce k dispozici funkční Julia instalaci.


0.1 Komentáře

Cokoliv za znakem # je ignorováno až do konce řádku.

Cokoliv mezi #= a =# je ignorováno i napříč více řádky.

Vyhodnocení kódu v následujících dvou buňkách proto nevygeneruje žádný výstup a v třetí buňce jsme skryli komentář přímo do argumentu jedné funkce (nakolik je toto vhodné je otázkou, je to ale možnost).

2 + 2 # this text is commented out
1 + 1
2
#= this
stuff is
IGNORED
=#

Tato syntaxe umožňuje poněkud divoké konstrukce, jako například:

sin(1.0 #= radians, obviously! =#)
0.8414709848078965

0.2 Čísla, řetězce, pravdivostní hodnoty

Často potřebujeme pracovat s čísly, řetězci a pravdivostními hodnotami. V tomto aspektu se Julia příliš neliší od ostatních programovacích jazyků.

Číselné literály mají standardní očekávaný tvar:

# Integer
123
123
1_000_000
1000000
1_234.123_456
1234.123456
# Float
12.2
12.2
# Float
12.0
12.0

Nebo, pro minimalisty:

# Float
.1
0.1
# Float
1.
1.0
1
1

Dále je k dispozici tzv. "vědecký" tvar strojového čísla:

# Float
1.23e-10
1.23e-10
1e-10
1.0e-10

K zápisu komplexních čísel je potřeba mít možnost vyjádřit imaginární část komplexního čísla. Toho docílíme přidáním im k číselnému literálu:

# Complex number with integer real and imaginary parts
10 + 5im
10 + 5im
# Complex number with float real and imaginary parts
3. + 1im
3.0 + 1.0im
0im
0 + 0im
1im
0 + 1im

im má význam i samo o sobě jako imaginární jednotka:

im
im

Řetězce uvozujeme pomocí dvojitých uvozovek:

"Hello!"
"Hello!"

Pozor: Ke spojování řetězců se používá operátor * (řetězce se spojováním tvoří pologrupu, v které se tradičně binární operace značí multiplikativně -- tečka, vynechání symbolu). K několikanásobnému opakování jednoho řetězce pak exponenciace pomocí operátoru ^.

"Budiž" * " " * "světlo"
"Budiž světlo"
("Budiž" * " " * "světlo") ^ 5
"Budiž světloBudiž světloBudiž světloBudiž světloBudiž světlo"
"~" ^ 25
"~~~~~~~~~~~~~~~~~~~~~~~~~"

Pozor, operace + mezi řetězci není definována.

"A" + "B"
MethodError: no method matching +(::String, ::String)
The function `+` exists, but no method is defined for this combination of argument types.
String concatenation is performed with * (See also: https://docs.julialang.org/en/v1/manual/strings/#man-concatenation).

Closest candidates are:
  +(::Any, ::Any, ::Any, ::Any...)
   @ Base operators.jl:653
  +(::Bool, ::Complex{Bool})
   @ Base complex.jl:301
  +(::Bool, ::Bool)
   @ Base bool.jl:169
  ...


Stacktrace:
 [1] top-level scope
   @ In[24]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Jednoduché uvozovky pojmou jenom jeden znak (char).

'a'
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
'ψ'
'ψ': Unicode U+03C8 (category Ll: Letter, lowercase)

Čili následující výraz vyústí v chybu!

'ab'
ParseError:
# Error @ In[27]:1:2
'ab'
#└┘ ── character literal contains multiple characters

Stacktrace:
 [1] top-level scope
   @ In[27]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Regulární výrazy se také zapisují do složených závorek, pouze jsou navíc prefixovány písmenkem r. (Julia používá knihovnu PCRE.)

r"[abc]+"
r"[abc]+"

Pravdivostní hodnoty zapisujeme true (pravda) a false (nepravda).

K dispozici dále máme standardní binární operátory (sčítání +, násobení *, dělení /), závorky a logické operátory (and && a or || a negaci !). K umocňování se používá symbol stříšky ^.

2 ^ 8
256

Jednou Julia zajímavostí, která stojí za zmínku v tento okamžik, je zkrácené psaní součinů bez znaménka *, podobně jak jsme tomu zvyklí v matematické syntaxi. K přiřazení hodnoty do proměnné se standardně používá rovnítka.

x = 10

5x + 2(10 + x)
90
5 * x + 2 * (10 + x)
90

Pozor na pořadí, cifra na konci názvu se už jako násobení neinterpretuje. Následující kód bez dříve definované proměnné x5 vyvolá chybu.

x5
UndefVarError: `x5` not defined in `Main`
Suggestion: check for spelling errors or missing imports.

Stacktrace:
 [1] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

U zkráceného zápisu strojového čísla je také nutné násobení uvést explicitně, tedy:

5. * x
50.0

Ale pokus o vyhodnocení následující buňky skončí chybou.

5.x
ParseError:
# Error @ In[36]:1:1
5.x
└┘ ── numeric constant cannot be implicitly multiplied because it ends with `.`

Stacktrace:
 [1] top-level scope
   @ In[36]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Pokud bychom uvedli i část za desetinnou tečkou, tak již vše dopadne tak jak čekáme.

5.0x
50.0

Naopak 4y se parsuje jako 4 * y (číselný literál na začátku), jenomže proměnná y zatím neexistuje – výraz proto skončí chybou UndefVarError.

4y
UndefVarError: `y` not defined in `Main`
Suggestion: check for spelling errors or missing imports.

Stacktrace:
 [1] top-level scope
   @ In[38]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Druhou zajímavostí může být způsob psaní víceřádkových výrazů. V jiných jazycích je k tomu většinou potřeba umístit speciální znak na konec řádku (například /). V Julia stačí řádek zakončit binárním operárorem, jako v tomto příkladě:

1 + 2 +
    3 + 4
10

Nesmíme ovšem zakončit řádek pouhým binárním operátorem.

1 + 2 +
ParseError:
# Error @ In[43]:1:8
1 + 2 +
#      └ ── premature end of input

Stacktrace:
 [1] top-level scope
   @ In[43]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

nebo

Podobně tomu je třeba i s operátorem porovnání ==:

true ==
  false
false

Další zajímavostí může být chování dělení /. I u integerů dělitelných beze zbytku vždy vyústní ve float:

4 / 2
2.0

Následující hodnota není rovna 13\frac{1}{3}:

1 / 3
0.3333333333333333

Vedle symbolu / pro dělení můžeme použít k stejnému účelu i \. Toho se využívá zejména při práci s maticemi (násobení inverzí z dané strany).

1 \ 3
3.0

Je dobré se ale vyhnout nepřehledným výrazům jako například (/ a \ mají stejnou prioritu, ale jsou zleva asociativní, tj. výrazy a X b X c, kde X je \ nebo / jsou interpretovány jako (a X b) X c):

1 / 2 \ 3
6.0
(1 / 2) \ 3
6.0
1 / (2 \ 3)
0.6666666666666666

Pokud potřebujeme celočíselné dělení, použijeme k tomu metodu div.

div(4, 2)
2
div(1, 3)
0

Případně je možné použít následující speciální symbol (\div a tabulátor):

4 ÷ 2
2
2 ÷ 4
0

K výpočtu zbytku po celočíselném dělení máme k dispozici operátor % nebo metodu mod (dále je zde mod1 a mod2pi):

5 % 3
2
mod(3, 2.0)
1.0
mod(4, 2)
0
mod1(4, 5)
4
mod2pi(7.0)
0.7168146928204135

Případně můžeme pracovat v exaktní aritmetice racionálních čísel.

# Rational
4 // 2
2//1
1 // 3 + 2 // 5
11//15
1 // 3 * 6 // 5
2//5

Během semestru se k této problematice vrátíme podrobněji.


0.3 Symboly

Vedle řetězců ještě narazíme na symboly, v podstatě pojmenované integer hodnoty. Tímto způsobem se často předávají různé parametry vizualizací/algoritmů. Symbol je uvozený dvojtečkou za níž následují písmena, čísla, nebo podtržítka.

:my_symbol_no_1

0.4 Pole, tuply, slovníky

Vícerozměrným polím se budeme věnovat velmi podrobně v páté lekci. V tento okamžik si ukažme několik způsobů jak jednoduše vytvářet vektory (jednorozměrné pole) a matice (dvourozměrné pole). K zápisu polního literálu se používá hranatých závorek. Prvky můžeme oddělovat čárkami, středníky, nebo bílými znaky. Všimněte si rozdílu v rozměrech:

a = [1, 2, 3, 4, 5]
5-element Vector{Int64}:
 1
 2
 3
 4
 5

Pozor! Indexujeme od 11 (lze změnit)!

a[1]
1
a[0]
BoundsError: attempt to access 5-element Vector{Int64} at index [0]

Stacktrace:
 [1] throw_boundserror(A::Vector{Int64}, I::Tuple{Int64})
   @ Base ./essentials.jl:14
 [2] checkbounds
   @ ./essentials.jl:389 [inlined]
 [3] getindex(A::Vector{Int64}, i::Int64)
   @ Base ./essentials.jl:969
 [4] top-level scope
   @ In[68]:1
 [5] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
a[-1]
BoundsError: attempt to access 5-element Vector{Int64} at index [-1]

Stacktrace:
 [1] throw_boundserror(A::Vector{Int64}, I::Tuple{Int64})
   @ Base ./essentials.jl:14
 [2] checkbounds
   @ ./essentials.jl:389 [inlined]
 [3] getindex(A::Vector{Int64}, i::Int64)
   @ Base ./essentials.jl:969
 [4] top-level scope
   @ In[69]:1
 [5] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
a[length(a)]
5
a[end]
5
a[end - 1]
4
[1 2 3 4 5]
1×5 Matrix{Int64}:
 1  2  3  4  5
[1, 2.4]
2-element Vector{Float64}:
 1.0
 2.4
[1, 3.4, "Ahoj!"]
3-element Vector{Any}:
 1
 3.4
  "Ahoj!"

Další řádek matice můžeme oddělit koncem řádku.

[
    1.0 2.0 3.0
    4.0 5.0 6.0
]
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 4.0  5.0  6.0

Nebo lze k tomuto účelu použít středník. Pozor na rozměry, následující pokus z dobrých důvodů selže!

[ 1 2 3; 4 5 6]
2×3 Matrix{Int64}:
 1  2  3
 4  5  6
[
    1 2 3;
    4 5 6
]
2×3 Matrix{Int64}:
 1  2  3
 4  5  6
[ 1 2 3; 4 5 ]
ArgumentError: argument count does not match specified shape (expected 6, got 5)

Stacktrace:
 [1] hvcat(::Tuple{Int64, Int64}, ::Int64, ::Vararg{Int64})
   @ Base ./abstractarray.jl:2225
 [2] top-level scope
   @ In[85]:1
 [3] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Následující kód vytvoří čtvercovou matici.

A = [1 2; 3 4]
2×2 Matrix{Int64}:
 1  2
 3  4

Pokud jste vytvářeli matice v Pythonu (Numpy), tak by vás možná napadlo použít "vnořené listy". To v Julia ovšem bude mít jiný význam, dostanete vektor (jednorozměrné pole), které má jako prvky matice:

[[1 2], [3 4]]
2-element Vector{Matrix{Int64}}:
 [1 2]
 [3 4]

Tento objekt se už nechová jako matice, nebude na něm správně fungovat maticové násobení a další metody lineární algebry!

[[1 2]; [3 4]]
2×2 Matrix{Int64}:
 1  2
 3  4
[
 [1 2]
 [3 4]
]
2×2 Matrix{Int64}:
 1  2
 3  4

Během semestru se různým metodám vytváření matic budeme věnovat. Toho, čeho jsme se snažili dosáhnout v buňce výše, bychom dosáhli pomocí metody vcat (existuje i hcat):

vcat([1 2], [3 4])
2×2 Matrix{Int64}:
 1  2
 3  4
hcat([1 2], [3 4])
1×4 Matrix{Int64}:
 1  2  3  4

Pozor na rozměry (mezery vs. čárky):

vcat([1, 2], [3, 4])
4-element Vector{Int64}:
 1
 2
 3
 4
hcat([1, 2], [3, 4])
2×2 Matrix{Int64}:
 1  3
 2  4

Tímto způsobem můžeme vytvářet v podstatě blokový zápis matic známý z matematiky.

A = [1 2; 3 4]
B = [0 0; 0 0]

[ A B; B A ]
4×4 Matrix{Int64}:
 1  2  0  0
 3  4  0  0
 0  0  1  2
 0  0  3  4

Pole jsou měnitelné (mutable) objekty, jsou to kontejnery nesoucí objekty jistého druhu (typu, viz níže), které můžeme modifikovat. K prvkům pole přistupujeme pomocí hranatých závorek, ve výchozím stavu jsou indexovány od jedné. Řádkové a sloupcové indexy mají stejný význam jako v matematice.

A
2×2 Matrix{Int64}:
 1  2
 3  4
A[1, 2] = 999 # první řádek, druhý sloupec
A
2×2 Matrix{Int64}:
 1  999
 3    4
A[2, 2]
4

Ovšem pozor na typy objektů, které chceme jako prvky použít.

A[1, 1] = "Ahoj!"
MethodError: Cannot `convert` an object of type String to an object of type Int64
The function `convert` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  convert(::Type{T}, ::T) where T<:Number
   @ Base number.jl:6
  convert(::Type{T}, ::Number) where T<:Number
   @ Base number.jl:7
  convert(::Type{T}, ::T) where T
   @ Base Base_compiler.jl:136
  ...


Stacktrace:
 [1] setindex!(::Matrix{Int64}, ::String, ::Int64, ::Int64)
   @ Base ./array.jl:1030
 [2] top-level scope
   @ In[103]:1
 [3] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
A[1, 1] = 1.1
InexactError: Int64(1.1)

Stacktrace:
 [1] Int64
   @ ./float.jl:923 [inlined]
 [2] convert
   @ ./number.jl:7 [inlined]
 [3] setindex!(::Matrix{Int64}, ::Float64, ::Int64, ::Int64)
   @ Base ./array.jl:1030
 [4] top-level scope
   @ In[104]:1
 [5] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
A[1, 1] = 5.0
5.0
A
2×2 Matrix{Int64}:
 5  999
 3    4

Tuple (tento termín do češtiny překládat nebudeme -- nejblíže by k němu měla "uspořádaná nn-tice") je v podstatě neměnná (immutable) verze jednorozměrného pole. K jejímu literálnímu zápisu slouží kulaté závorky.

() # prázdný tuple
()

Pozor:

(1,) # tuple s jedním prvkem
(1,)
(1)  # jakýsi výraz, jehož hodnota je 1
1

Tuple obsahující tři prvky.

t = (1, 2, 3)
(1, 2, 3)

Prvky tuple se indexují také pomocí hranaté závorky a index běží od 11. Prvky tuple nelze modifikovat.

t[1]
1
t[1] = 10
MethodError: no method matching setindex!(::Tuple{Int64, Int64, Int64}, ::Int64, ::Int64)
The function `setindex!` exists, but no method is defined for this combination of argument types.

Stacktrace:
 [1] top-level scope
   @ In[114]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Oproti tomu:

a = [1, 2]

a[1] = 10

a
2-element Vector{Int64}:
 10
  2

"NamedTuple"

t = (a = 1, b = 2)
(a = 1, b = 2)
t[1]
1
t[:b]
2
t[:c] = 10
MethodError: no method matching setindex!(::@NamedTuple{a::Int64, b::Int64}, ::Int64, ::Symbol)
The function `setindex!` exists, but no method is defined for this combination of argument types.

Stacktrace:
 [1] top-level scope
   @ In[124]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Poslední základní, a často používanou, datovou strukturou je slovník (dictionary). Ten explicitně můžeme vytvořit následujícím způsobem

d = Dict(1 => "Hello!", 2 => "Goodbye!")
Dict{Int64, String} with 2 entries:
  2 => "Goodbye!"
  1 => "Hello!"
d[1]
"Hello!"
d[2]
"Goodbye!"
d[3] = "Hello?"

d
Dict{Int64, String} with 3 entries:
  3 => "Hello?"
  2 => "Goodbye!"
  1 => "Hello!"
d[5]

Pozor, neexistující klíč se automaticky nedoplní – takový přístup skončí chybou KeyError.

Všimněte si, že u tohoto slovníku d jsou typy hodnot klíčů a hodnot omezené (na integery, resp. řetězce). Pokud chceme mít slovník, který pojme prakticky cokoliv, můžeme toho dosáhnout takto:

d2 = Dict{Any, Any}()
Dict{Any, Any}()
d2[2] = "Ahoj!"
d2["a"] = 42im
d2
Dict{Any, Any} with 2 entries:
  2   => "Ahoj!"
  "a" => 0+42im

Pozor:

d2[2.0] = "Ha!"
d2
Dict{Any, Any} with 2 entries:
  2.0 => "Ha!"
  "a" => 0+42im
d2[2]
"Ha!"

Ooops.


0.5 Funkce, resp. metody

Funkcemi, resp. metodami, se budeme ve větší hloubce zabývat v příští (čtvrté) lekci.

Nyní si alespoň uveďme dva základní způsoby, jak definovat nové funkce (ve skutečnosti metody, z pohledu Julia). První je jednořádkový:

f(#= arguments =#) = #= expression =#

Druhý víceřádkový, vhodnější pro funkce s větším tělem:

function f(#= arguments =#)
    #= body =#
end

Dále můžeme vytvořit anonymní funkci a tu přiřadit do proměnné (lambda funkce):

f = (#= arguments =#) -> #= expression =#

Například tedy můžeme definovat:

f(x, y) = x + y

function g(x)
    x + 1
end

h = (x, y) -> x + y + 1;

Tyto funkce pak můžeme zkusmo vyhodnotit na několika vstupech.

f(1, 2)
3
f(1.1, 2.45)
3.5500000000000003
f("a", "b")
MethodError: no method matching +(::String, ::String)
The function `+` exists, but no method is defined for this combination of argument types.
String concatenation is performed with * (See also: https://docs.julialang.org/en/v1/manual/strings/#man-concatenation).

Closest candidates are:
  +(::Any, ::Any, ::Any, ::Any...)
   @ Base operators.jl:653
  +(::Bool, ::Complex{Bool})
   @ Base complex.jl:301
  +(::Bool, ::Bool)
   @ Base bool.jl:169
  ...


Stacktrace:
 [1] f(x::String, y::String)
   @ Main ./In[132]:1
 [2] top-level scope
   @ In[135]:1
 [3] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
f([1, 2], [3, 4])
2-element Vector{Int64}:
 4
 6
g(1.2345)
2.2344999999999997
h(5, 6)
12

Návratová hodnota je hodnota naposledy vyhodnoceného výrazu, nebo ji lze explicitně předat pomocí klíčového slova return.

Dále vedle standardního způsobu zápisu volání pomocí závorek (prefixová notace) můžeme použít operátoru |> k postfixovému zápisu volání (v Mathematica //):

1.0 |> sin
0.8414709848078965
sin(1.0)
0.8414709848078965
1.0 |> sin |>
       x -> x^2 |>
       cos
0.7596162788336037

Tuto operaci lze řetězit, případně i rozepsat pro přehlednost na více řádků.

2 |> x -> x^2 |> sqrt
2.0

Pozor, známý symbol | má význam bitového OR.

1 | 2
3

0.6 Metoda println

V Jupyteru/JupyterLabu se při vyhodnocení buňky vypíše hodnota naposledy vyhodnoceného výrazu. Pokud chceme vypsat více hodnot, můžeme k tomu použít metodu println. Pokud program spouštíme z příkazové řádky, pak se tento výstup vypisuje na standardní výstup. Všimněte si i grafického znázornění tohoto chování u levého okraje buňky:

println(1)
println(2)
1 + 2
1
2
3

Pro hrubé vypisování různých hlášení a informací se hodí řetězcová interpolace, pro kterou se v Julia využívá symbol $:

x = 10

println("The value of x is $x")
println("Jednotková cena je ", x, "\$")
println("The square root of x is $(sqrt(x))")
The value of x is 10
Jednotková cena je 10$
The square root of x is 3.1622776601683795

Alternativně println akceptuje více argumentů, které spojuje bez mezer:

x = 42

println("And the answer is: ", x)
And the answer is: 42

Později během semestru se budeme zabývat modulem Logging, který umožňuje podrobnější vypisování informací a hlášení o chybách. Dále je ve standardní knihovně k dispozici modul Printf poskytující makro @printf umožňující kontrolovat formátování výstupu (zaokrouhlování, počet cifer atd.).


0.7 Unicode znaky

Ve zdrojovém kódu Julia programu lze používat unicode znaky. K jejich snadnému zápisu lze použít LaTeX syntaxi (minimálně v Jupyteru/JupyterLabu, REPL i VSCode).

Napište LaTeX makro a poté stiskněte klávesu TAB.

K řadě "built in" metod, nebo i konstant, lze tímto způsobem přistupovat. Například:

π # \pi
π = 3.1415926535897...
2π
6.283185307179586

Pozor na speciální symbol přibližného porovnání (v buňce níže není =).

π ≈ 3.141592653897 # \approx
true
√4 # \sqrt
2.0

V některých případech tento přístup může být užitečný, ale mělo by se ho používat spíše poskrovnu.

Lehce zajímavě lze u symbolů používaných pro binární operace následně používat i infixovou notaci!

⊕(x, y) = mod(x + y, 5) # \oplus
⊕ (generic function with 1 method)
⊕(2, 3)
0
2 ⊕ 3
0
4 ⊕ 3
2

0.8 Konstanty

Konstanty definujeme pomocí klíčového slova const.

const AlmostPi = 3.0
3.0

Jejich hodnotu nelze jen tak přepsat – přiřazení do konstanty bez klíčového slova const skončí chybou (podívejte se na buňku níže).

AlmostPi = 1
invalid assignment to constant Main.AlmostPi. This redefinition may be permitted using the `const` keyword.

Stacktrace:
 [1] top-level scope
   @ In[163]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
AlmostPi
3.0

0.9 Středník

Pomocí středníku můžeme umístit více příkazů na jeden řádek a/nebo potlačit výstup.

a = 1; b = 2
2
a
1
a + b;

Cvičení

  1. Výše jsme viděli, že číslo π\pi odpovídá Julia konstantě se stejným symbolem. Jak je to s Eulerovým číslem?
  2. Pod jakými názvy najdete v Julia trigonometrické funkce, exponenciální funkce a logaritmus?
  3. Napadá vás nějaký základní konstrukt, vám dobře známý z jiných programovacích jazyků, který jsme nezmínili?
  4. Experimentálně prozkoumejte, jak se Julia chová vůči přetečení. Máme k dispozici i datové typy pro práci s čísli s většími rozsahy/přesností?
typemax(Int64) + 1
-9223372036854775808
typemin(Int64)
-9223372036854775808
typemax(Float64)
Inf
prevfloat(Inf)
1.7976931348623157e308
prevfloat(Inf) + 1.0
1.7976931348623157e308
prevfloat(prevfloat(Inf))
1.7976931348623155e308
nextfloat(1.0)
1.0000000000000002
Rational(nextfloat(1.0))
4503599627370497//4503599627370496
Rational(0.3)
5404319552844595//18014398509481984

1. Řídící struktury

Nyní se pustíme do prvního tématického bloku této lekce a probereme různé imperativní prvky jazyka Julia.

V této části by asi pro studenty, kteří již s programováním přišli do styku, nemělo být nic zásadně překvapivého a proto bude tento výklad spíše stručnější.


1.1 Podmínky (if-elseif-else)

Anatomie podmínky má nepřekvapivou strukturu:

if boolean_condition_1
    # ...
    # evaluates if boolean_condition_1 is true
    # ...
elseif boolean_condition_2
    # ...
    # evaluates if boolean_condition_1 is false AND boolean_condition_2 is true
    # ...
# ...
# possibly more elseifs
# ...
else
    # ...
    # evaluates if both boolean_condition_1 AND boolean_condition_2 are false
    # ...
end

If blok vrací hodnotu posledního vyhodnoceného výrazu.

Pozor! Výrazy použité v podmínkách skutečně musí mít hodnotu true nebo false. Následující experiment nedopadne dobře:

if 1
    println("Yay!")
end
TypeError: non-boolean (Int64) used in boolean context

Stacktrace:
 [1] top-level scope
   @ In[177]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Tuto chybu použijeme i k vysvětlení na první pohled možná kryptické informace v červeném chybovém výpisu. V závorce In[k]:r najdete číslo buňky k (v pořadí, v jakém byly buňky v tomto sezení vyhodnoceny) a řádek r, na kterém prvotní chyba nastala.


"Ternární if"

Dále je nám k dispozici "ternární operátor" ?:, jeho struktura je opět standardní a asi jste se s ní setkali i v jiných jazycích:

boolean_condition ? #= expression evaluated when true =# : #= expression evaluated when false =#

Cvičení

Absolutní hodnota reálného čísla je typicky definována předpisem ∣x∣={x,x≥0,−x,x<0.|x| = \begin{cases} x, & x \geq 0, \\ -x, & x < 0. \end{cases} Definujte odpovídající metodu my_abs v Julia.

my_abs(x) = x < 0 ? -x : x
my_abs (generic function with 1 method)

Podle očekávání dostaneme:

println(my_abs(10))
println(my_abs(-1.1))
println(my_abs(π))
10
1.1
π

Následující pokus ovšem zcela oprávněně selže:

my_abs("wtf")
MethodError: no method matching isless(::String, ::Int64)
The function `isless` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  isless(::Base.CoreLogging.LogLevel, ::Integer)
   @ Base logging/logging.jl:132
  isless(::Missing, ::Any)
   @ Base missing.jl:87
  isless(::AbstractFloat, ::Real)
   @ Base operators.jl:224
  ...


Stacktrace:
 [1] <(x::String, y::Int64)
   @ Base ./operators.jl:399
 [2] my_abs(x::String)
   @ Main ./In[178]:1
 [3] top-level scope
   @ In[180]:1
 [4] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Jak se s podobnými situacemi vypořádat si ukážeme v následující části této lekce o typech.


Logické operátory

Pro vytváření logických výrazů využitelných například v if podmínkách lze standardně použít závorky a operátory && (and) a || (or). Při vyhodnocení těchto výrazů se Julia snaží vyhodnotit co nejméně podvýrazů, tj. např v false && whatever se whatever nevyhodnocuje (short circuit evaluation). Následující dělení nulou nám projde nepovšimnuto:

isodd(2) && 1 / 0
false

Toho se občas v Julia používá ke kompaktnímu vyjádření podmínky:

iseven(2) && println("It is even!")
It is even!
nothing

Hodnota výrazu println("It is even!") je nothing, což je v těchto výrazech akceptovatelné a má za následek také hodnotu nothing, konkrétně:

true && nothing
false || nothing

Alternativně lze použít operátory & a |, které ale vždy vyhodnocují svoje argumenty. Pozor, tyto operátory mají také význam bitového and a or. Pro bitový xor je v infixové notaci k dispozici operátor ⊻ (LaTeX makro \xor).

1 & 2
2 | 1
2 ⊻ 3

A dělení nulou nám projde i teď :-D.

true & 1 / 0
1 / 0

Dělení nulou u desetinných čísel nevyvolá chybu – výsledkem je Inf (resp. NaN pro 0 / 0). U celočíselného dělení div naopak chybu dostaneme, jak uvidíte v buňce níže.

div(1, 0)

Pozor, následující výraz je zřejmě reprezentace nekonečna!

1 // 0
Float64(1 // 0)

Klíčová slova and nebo or zde neexistují.

true and true
and

1.2 Složené výrazy / bloky

Julia umožňuje vyhodnocování výrazů složit do bloků. Hodnotou bloku je hodnota naposledy vyhodnoceného výrazu. K dispozici máme víceřádkové bloky uvozené mezi begin a end:

u = 12

z = begin
    x = u + 1
    y = 2
    x + y
end

z

Nebo jednořádkové bloky, kde jednotlivé výrazy oddělujeme středníky ;. Závorky nejsou vždy nutné, ale značně zlepšují čitelnost:

z = (x = 1; y = 2; x + y)

z

1.3 Cykly (while a for)

Máme k dispozici v zásadě dva způsoby opakovaného vyhodnocování výrazů: for a while cyklus.


while cyklus

Tělo while cyklu se vyhodnocuje dokud podmínka za klíčovým slovem while vrací true:

j = 1

while j <= 3
    println(j)
    j += 1
end

println("---")
println(j)

for cykly a rozsahy (range)

Anatomie for cyklu je opět nepřekvapivá:

for j = iterator
  # ...
  # do stuff with j
  # ...
end

Zde iterator je cokoliv implementující Julia iterační protokol. Nejčastěji rozsah (range; budeme se jim věnovat později v lekci o polích) start:end, tuple, nebo pole (i vícerozměrné).

Místo symbolu = můžeme použít i slovíčko in nebo dokonce symbol ∈ (LaTeX \in). Konkrétní volba je spíše otázkou vkusu. Následuje několik elementárních ukázek.

for j = 1:3
    println(j)
end
1
2
3
1:3
1:3

V rozsahu lze případně i měnit délku kroku.

for j in 1:2:6
    println(j)
end
1
3
5

Můžeme pak snadno iterovat i v opačném pořadí.

for j = 5:-1:0
    println(j)
end
5
4
3
2
1
0

A konečně, příklad iterace přes prvky jednorozměrného pole, s drobnou reklamou na další téma této lekce.

for x ∈ [1, π, "string", :symbol]
    println(typeof(x))
end
Int64
Irrational{:π}
String
Symbol

K urychlení přechodu k dalšímu prvku můžeme použít klíčové slovo continue. Například, následující kód je zvrhlý způsob vypsání několika malých sudých čísel.

for n = 1:10
    isodd(n) && continue
    
    println(n)
end
2
4
6
8
10

K předčasnému opuštění cyklu slouží klíčové slovo break:

for n = 1:10
    n == 5 && break
    
    println(n)
end
1
2
3
4

Hodnoty range nemusí být nutně celočíselné, koncová hodnota se dopočítá. Pozor ale na reprezentaci desetinných čísel – rozsah nemusí končit přesně na požadované hodnotě:

0.1:0.1:6.0
0.1:0.1:6.0
[x for x in 0.1:0.5:6.0] # k této syntaxi se ještě dostaneme
12-element Vector{Float64}:
 0.1
 0.6
 1.1
 1.6
 2.1
 2.6
 3.1
 3.6
 4.1
 4.6
 5.1
 5.6

Cvičení: Naivní test prvočíselnosti

Implementujte "naivní" test prvočíselnosti: Má-li přirozené číslo nn netriviálního dělitele, pak tento nutně leží mezi 22 a n\sqrt{n} (včetně). (Proč?)

function my_test(n)
    k = 2
    while k^2 <= n
        if n % k == 0
            return false
        end

        k += 1
    end

    return true
end

Použití je pak nasnadě:

my_test(4)
my_test(13)
my_test(9)

Jak se chová my_test pro n = 1? Vyzkoušejte a případně funkci upravte tak, aby ošetřila i tento okrajový případ.

Pro úplnost: rozsah může vzniknout i z výrazu s funkcí. Např. 2:sqrt(5) vede na rozsah od 2 do přibližně 2.236, čili obsahuje jediný prvek. To mimochodem vysvětluje, proč při testu prvočíselnosti stačí projít dělitele 2 až floor(Int, sqrt(n)).

for k in 2:sqrt(5)
    println(k)
end
sqrt(4)

Cvičení: Řetězové zlomky

Mějme (n+1)(n+1)-tici (a0,a1,…,an)(a_0,a_1,\ldots,a_{n}). Následující výraz nazýváme (ukončeným) řetězovým zlomkem

a0+1a1+1a2+1a3+⋱+1an.a_0 + \frac{1}{a_1 + \frac{1}{a_2 + \frac{1}{a_3 + \ddots + \frac{1}{a_n}}}}.

Implementujte metodu, která spočte hodnotu tohoto zlomku.

function my_cf(seq)
    acc = seq[end]
    
    for k = (length(seq)-1):-1:1
        acc = seq[k] + 1 / acc
    end

    return acc
end
my_cf([2])
my_cf([1, 2])

Jaké hodnoty vám připomínají následující?

my_cf([2, 1, 2, 1, 1, 4, 1, 1, 6, 1, 1, 8, 1, 1, 10])
my_cf([2, 1, 2, 1])
my_cf([3, 7, 15, 1, 292, 1, 1, 1, 2, 1, 3, 1, 14, 2, 1, 1, 2, 2, 2, 2, 1, 84, 2, 1, 1])
my_cf([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])

1.4 Viditelnost proměnných (scoping)

Tomuto tématu se budeme věnovat jen krátce, ale stojí za to o něm vědět: viditelnost (scope) určuje, odkud je proměnná přístupná a kdy vzniká „nová" proměnná stejného jména.

Zjednodušeně v Julia platí:

  • Přiřazení na vrcholné úrovni (v Jupyteru tedy přímo do buňky) vytvoří globální proměnnou, viditelnou i v ostatních buňkách.
  • Bloky if a begin nevytvářejí nový rozsah.
  • Naopak těla cyklů (for, while), bloky try-catch, funkce a bloky let vytvářejí lokální rozsah; iterátor cyklu je vždy lokální proměnnou cyklu.

(Podrobněji viz Variables and Scoping.)

if nový rozsah nevytváří – proměnná odpoved přiřazená uvnitř podmínky je po návratu z podmínky viditelná:

x = 1

if x > 0
    odpoved = "ano"
end

odpoved
"ano"

Iterátor cyklu je naopak lokální. Pokud neexistuje globální proměnná stejného jména, i po skončení cyklu není definováno:

i = -1

for i = 1:3
    println(i)
end

i   # chyba – žádná globální proměnná `i` neexistuje
1
2
3
-1

Tělo cyklu (stejně jako while či try-catch) má v Julia „měkký" lokální rozsah. V interaktivním prostředí (REPL i Jupyter) se přiřazení do proměnné, která už jako globální existuje, projeví i do této globální proměnné; ve skriptu (souboru) je takový zápis naopak chybou. Spoléhat se na to nelze, a mutace globální proměnné uvnitř cyklu je navíc pomalá – kompilátor globály neumí optimalizovat (typ se může změnit kdykoliv, dokonce v jiné buňce). Dobrá praxe:

  • kód s cykly uzavřete do funkce, která pracuje se svými lokálními proměnnými (viz příští lekce), a
  • pokud už globální proměnnou v cyklu měnit musíte, uveďte explicitně global.
total = 0

for j = 1:10
    global total += j
end

total

Jasně ohraničený blok s vlastním rozsahem poskytuje let – proměnné vytvořené uvnitř se „venku" neprojeví:

velikost = 99            # globální proměnná

let velikost = 10
    println("uvnitř let: ", velikost)
end

velikost                 # globální `velikost` se nezměnila

1.5 Výjimky a ošetření chyb (try-catch)

Když při vyhodnocování dojde k chybě, Julia vyhodí výjimku (exception). Dokud ji nikde nezachytíme, výpočet se přeruší a v buňce uvidíme červený chybový výpis.

Chyby lze v programech zachytávat a ošetřovat pomocí bloku try-catch:

try
    # ... kód, který může selhat ...
catch e
    # ... co udělat, když selže (e je objekt výjimky; `catch e` je volitelný) ...
finally
    # ... provede se vždy, ať se stalo cokoliv (volitelné) ...
end

(Další detaily v Control Flow.)

try
    div(1, 0)          # celočíselné dělení nulou
catch e
    println("Zachyceno: ", e)
end
try
    sqrt(-1)           # odmocnina ze záporného čísla
catch e
    println(typeof(e), ": ", e)
end

Vlastní chybu vyhodíme pomocí error (obecněji pak throw):

function overni_kladnost(x)
    x > 0 || error("očekáváme kladné číslo, dostali jsme $x")
    sqrt(x)
end

try
    overni_kladnost(-3)
catch e
    println("Zachyceno: ", e)
end

Blok finally se vykoná vždy, ať už v try došlo k chybě či nikoliv:

try
    div(1, 0)
catch e
    println("v catch: ", e)
finally
    println("v finally – provede se vždy")
end

Na závěr dvě doporučení. Výjimky používejte pro skutečně výjimečné stavy – je-li chybnému stavu snadné předejít podmínkou, napište podmínku. A zachycené výjimky nikdy tiše „nepolykejte" (např. catch bez jakéhokoliv zpracování) – chyba pak zmizí beze stopy.

Cvičení

Napište funkci safe_sqrt, která pro záporný argument vrátí nothing a jinak sqrt(x). Implementujte ji nejprve pomocí try-catch, poté bez výjimek (podmínkou). Která varianta je podle vás čitelnější a proč?


2. Typový systém

Za hlavní znaky Julia považuji její typový systém (tím se budeme zabývat zde) a multiple dispatch (kterým se budeme zabývat hned v příští lekci).

  • Julia patří mezi dynamicky typované jazyky. Typy objektů mohou tedy být známy teprve až v okamžiku běhu programu.

  • Julia ale umožňuje používat typové anotace a tím (nejen) pomoci LLVM kompilátoru při optimalizaci kódu. Dále je pomocí typových anotací možné vytvářet více metod jedné funkce, které mohou vykonávat jinou činnost podle typu argumentů. Tímto způsobem lze vlastně volit vždy nejvhodnější algoritmus pro danou situaci. Toto je zásadní prvek návrhu Julia, tzv. multiple dispatch.

  • V Julia mezi typy existuje explicitní hierarchie. Julia typy mohou být dále parametrizované jinými typy.

Hned na začátku této části lekce upozorněme na metodu typeof, pomocí které může zvídavý programátor zjistit typ objektu. Například můžeme prozkoumat typy objektů, s kterými jsme pracovali v úvodu této lekce.

typeof(1)
Int64
typeof(1.0)
Float64
typeof(1im)
Complex{Int64}
typeof(2.0 + 4.0im)
ComplexF64 (alias for Complex{Float64})

V tomto případě jde o parametrický typ! Za chvilku se jim budeme věnovat podrobněji.

typeof(Inf)
Float64
typeof("Hello!")
String
typeof([1.0, 2.0])
Vector{Float64} (alias for Array{Float64, 1})
typeof([1 2; 3 4])
Matrix{Int64} (alias for Array{Int64, 2})
typeof(:symbol)
Symbol
typeof(π)
Irrational{:π}
typeof(Float64)
DataType
typeof(DataType)
DataType
typeof(nothing)
Nothing
typeof(Nothing)
DataType
typeof(Any)
DataType

2.1 Abstraktní a konkrétní typy

Abstraktní typy jsou, abstraktní... Každý abstraktní typ má svůj "supertyp" a může mít více "podtypů". Vztah "být podtypem" je tranzitivní.

Nelze vytvořit objekt abstraktního typu, tyto typy tedy slouží jen jako vrcholy v hierarchické stromové struktuře typového systému. Na vrcholu této hierarchie je typ Any, ten je supertypem všech typů. Význam abstraktních typů je efektivně následující: lze pomocí nich sdružit "příbuzné" typy a lze pomocí nich kontrolovat, která metoda funkce se ve finále zavolá (multiple dispatch).

Deklarace abstraktního typu je jednoduchá (v prvním případě bude supertypem typ Any):

abstract type #=type_name=# end
abstract type #=type_name=# <: #=supertype=# end

Typickým příkladem abstraktních typů mohou být následující číselné typy (ukázka ze zdrojového kódu Julia, velká část je napsaná přímo v Julia):

abstract type Number end
abstract type Real     <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer  <: Real end
abstract type Signed   <: Integer end
abstract type Unsigned <: Integer end

Pomocí operátoru <: lze navíc i testovat, zda daný typ je podtypem jiného typu.

Real <: Number
true
Real <: Any
true
Any <: Any
true
Nothing <: Any
true

Rovnost. Rozlišujeme tři operátory porovnání: == (hodnotová rovnost), isequal (rovnost, která umí rozlišit NaN od sebe a 0.0 od -0.0) a === (identita objektu, u neměnných "bitových" hodnot splývá s hodnotovou rovností):

println(1 == 1.0)
println(1 === 1.0)
println(NaN == NaN)
isequal(NaN, NaN)
true
false
false
true

Možná překvapivě, z čistě matematického pohledu -- nepleťte <: s inkluzí odpovídajících množin:

Real <: Complex
false
Complex <: Number
true

Konkrétní typy, tj. typy, které již lze instanciovat, nemohou mít další konkrétní podtypy. Mezi konkrétní typy patří například různé číselné typy jako Int64 nebo Float64.

Int64 <: Real
true
Int64 <: AbstractFloat
false
Int64 <: Integer
true

Přirozeně se nabízí otázka, jakého typu je typ?

typeof(Int64)
DataType
typeof(DataType)
DataType
DataType <: Any
true

Užitečná také může být metoda supertypes, resp. supertype, která nám odhalí supertypy daného typu.

supertypes(Float64)
(Float64, AbstractFloat, Real, Number, Any)
supertype(Float64)
AbstractFloat
supertypes(Int64)
(Int64, Signed, Integer, Real, Number, Any)
supertypes(String)
(String, AbstractString, Any)

Na "druhou stranu" máme subtypes se zřejmým významem.

subtypes(AbstractFloat)
5-element Vector{Any}:
 BigFloat
 Core.BFloat16
 Float16
 Float32
 Float64
subtypes(Signed)
6-element Vector{Any}:
 BigInt
 Int128
 Int16
 Int32
 Int64
 Int8
subtypes(Integer)
3-element Vector{Any}:
 Bool
 Signed
 Unsigned
subtypes(Unsigned)
5-element Vector{Any}:
 UInt128
 UInt16
 UInt32
 UInt64
 UInt8

2.2 Primitivní typy

Primitivní typy jsou konkrétní typy, které lze považovat za dále datově nedělitelné. Tj. objektům těchto typů v paměti náleží data, která nelze dále dělit (mějte na mysli binární typy jako Int64 nebo Float64). Běžný uživatel pravděpodobně nebude mít potřebu definovat vlastní primitivní typy, i když by mohl. Podrobněji se jim zde věnovat nebudeme.

V Julia jsou například strojová čísla s různým počtem bitů definována následovně:

primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end

primitive type Bool <: Integer 8 end
primitive type Char <: AbstractChar 32 end

primitive type Int8    <: Signed   8 end
primitive type UInt8   <: Unsigned 8 end
primitive type Int16   <: Signed   16 end
primitive type UInt16  <: Unsigned 16 end
primitive type Int32   <: Signed   32 end
primitive type UInt32  <: Unsigned 32 end
primitive type Int64   <: Signed   64 end
primitive type UInt64  <: Unsigned 64 end
primitive type Int128  <: Signed   128 end
primitive type UInt128 <: Unsigned 128 end

Příkladem konkrétního typu, který není primitivní, jsou například datové typy modelující komplexní čísla.


2.3 Vsuvka: Deklarace typu pomocí operátoru ::

Operátor :: má v Julia dva významy.

Za každým výrazem (expression) lze pomocí :: deklarovat, jaký typ má mít jeho hodnota. Kompilátor tuto informaci bere do úvahy a často tak můžeme odhalit nesprávné chování našeho programu ještě před jeho spuštěním (resp. vyhodnocením výrazu).

Ukažme si to na následujících příkladech. Nejprve kontrola typu hodnoty výrazu:

x = 1

(x + 2)::Int64 # součet je Int64
3
x = 2.0

(x + 2)::Int64 # součet by sice přirozeně šlo převést na Int64, ale není to Int64
TypeError: in typeassert, expected Int64, got a value of type Float64

Stacktrace:
 [1] top-level scope
   @ In[3]:3
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
x = 2.5

(x + 3)::Int64 # zde ani není šance na převod
TypeError: in typeassert, expected Int64, got a value of type Float64

Stacktrace:
 [1] top-level scope
   @ In[4]:3
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
x = 2

(x + 4)::Integer
6

Pokud :: použijeme za symbolem lokální proměnné na levé straně přiřazení, tak tím deklarujeme typ její hodnoty, který nelze měnit.

Pokud hodnota daného typu není, tak se ji Julia nejprve pokusí na požadovaný typ převést.

function F(x)
    y::Int64 = 10x - 3

    #
    # ...
    #
    
    return y
end
F (generic function with 1 method)
F(2) # aritmetika probíhá v Int64
17
F(0.5) # výsledek lze přirozeně převést na Int64
2
F(2.22)
InexactError: Int64(19.200000000000003)

Stacktrace:
 [1] Int64
   @ ./float.jl:923 [inlined]
 [2] convert
   @ ./number.jl:7 [inlined]
 [3] F(x::Float64)
   @ Main ./In[6]:2
 [4] top-level scope
   @ In[9]:1
 [5] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Podobně lze deklarovat i typ návratové hodnoty funkce. Zde platí podobné poznámky jako u deklarace typu lokální proměnné.

function g(x)::Int64
    return 2x
end
g (generic function with 1 method)
g(10)
20
g(4.0)
8

Všimněte si, že:

2 * 4.0
8.0

Konečně, selhávající příklad:

g(3.33)
InexactError: Int64(6.66)

Stacktrace:
 [1] Int64
   @ ./float.jl:923 [inlined]
 [2] convert
   @ ./number.jl:7 [inlined]
 [3] g(x::Float64)
   @ Main ./In[10]:2
 [4] top-level scope
   @ In[14]:1
 [5] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

V další sekci se pomocí tohoto operátoru deklaruje typ atributu složeného typu. Dále na tento operátor narazíme při uvádění typu argumentu funkce (resp. metody), k tomu se budeme věnovat přímo v lekci o funkcích (resp. metodách).

H(x::Int64)   = 42x
G(x::Float64) = x / 2;
H(1)
42
H(1.0)
MethodError: no method matching H(::Float64)
The function `H` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  H(::Int64)
   @ Main In[15]:1


Stacktrace:
 [1] top-level scope
   @ In[17]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
G(1.0)
0.5
G(1)
MethodError: no method matching G(::Int64)
The function `G` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  G(::Float64)
   @ Main In[15]:2


Stacktrace:
 [1] top-level scope
   @ In[19]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
H("Ahoj!")
MethodError: no method matching H(::String)
The function `H` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  H(::Int64)
   @ Main In[15]:1


Stacktrace:
 [1] top-level scope
   @ In[20]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
G("Ahoj!")
MethodError: no method matching G(::String)
The function `G` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  G(::Float64)
   @ Main In[15]:2


Stacktrace:
 [1] top-level scope
   @ In[21]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

2.4 Složené typy

S tímto typem typu (heh!) přijde běžný uživatel pravděpodobně více do styku. Nejspíše bude vlastní složené typy sám definovat.

Složený typ definujeme pomocí klíčového slova struct a uvedeme, jaké atributy (případně jakých typů) má. Neuvedení typu je ekvivalentní uvedení ::Any:

struct #= Type name =#
    #= field name =#::Type
    # ...
end

Pozor, objekty tohoto typu jsou neměnitelné (immutable). Lze ovšem případně měnit jejich měnitelné (mutable) atributy.

K vytvoření mutable typu stačí použít mutable struct místo struct.

Instanci objektu vytvoříme pomocí konstuktoru, který má stejné jméno jako typ a jako argumenty bere popořadě v definici uvedené atributy. K atributům daného objektu pak přistupujeme pomocí operátoru ..

Následuje jednoduchý příklad ilustrující zmíněné skutečnosti:

struct ImmutableType
    a::Integer
end

mutable struct MutableType
    a::Integer
    b::String
end
typeof(ImmutableType)
DataType
supertypes(ImmutableType)
(ImmutableType, Any)
struct MyNumber <: Number
    a::Integer
end
supertypes(MyNumber)
(MyNumber, Number, Any)
MyNumber <: Number
true
x = ImmutableType(1)
println(x.a)

x.a = 10

println(x.a)
1
setfield!: immutable struct of type ImmutableType cannot be changed

Stacktrace:
 [1] setproperty!(x::ImmutableType, f::Symbol, v::Int64)
   @ Base ./Base_compiler.jl:61
 [2] top-level scope
   @ In[28]:4
 [3] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
x
ImmutableType(1)
x = MutableType(1, "Ahoj!")
println(x.a)
println(x.b)

x.a = 10

println(x.a)
1
Ahoj!
10
x
MutableType(10, "Ahoj!")
typeof(x)
MutableType
typeof(MutableType)
DataType
MutableType(1, 1)
MethodError: Cannot `convert` an object of type Int64 to an object of type String
The function `convert` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  convert(::Type{String}, ::String)
   @ Base essentials.jl:478
  convert(::Type{T}, ::T) where T<:AbstractString
   @ Base strings/basic.jl:227
  convert(::Type{T}, ::AbstractString) where T<:AbstractString
   @ Base strings/basic.jl:228
  ...


Stacktrace:
 [1] MutableType(a::Int64, b::Int64)
   @ Main ./In[22]:6
 [2] top-level scope
   @ In[34]:1
 [3] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
MutableType(1.0, "Ahoj")
MutableType(1, "Ahoj")
MutableType(1.1, "Ahoj")
InexactError: Int64(1.1)

Stacktrace:
 [1] Int64
   @ ./float.jl:923 [inlined]
 [2] Integer
   @ ./boot.jl:1044 [inlined]
 [3] convert
   @ ./number.jl:7 [inlined]
 [4] MutableType(a::Float64, b::String)
   @ Main ./In[22]:6
 [5] top-level scope
   @ In[38]:1
 [6] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

2.5 Parametrické typy

V definici abstraktního i složeného typu lze využívat parametry, které mohou být typy, nebo integery či symboly. Toto chování jsme už viděli například u polí, kde matice s Int64 prvky byla typu Matrix{Int64}, což je alias pro Array{Int64, 2}. Tímto způsobem můžeme specifikovat, jaké objekty je pole, resp. matice, schopno pojmout.

Parametrizaci složeného typu uvedeme v složených závorkách za jménem typu. V této definici není hodnota typu T nijak omezena:

struct ParametricType{T}
    a::T
end

Výchozí konstruktor (o nich více později) je metoda stejného jména jako typ. Všimněte si, že parametrický typ nemusíme explicitně uvádět, Julia ho dokáže odvodit z hodnoty argumentu.

x = ParametricType(10)
ParametricType{Int64}(10)
x.a
10
ParametricType(2.5)
ParametricType{Float64}(2.5)
ParametricType([1 2; 3 4])
ParametricType{Matrix{Int64}}([1 2; 3 4])
ParametricType("String")
ParametricType{String}("String")
ParametricType(ParametricType)
ParametricType{UnionAll}(ParametricType)

Ale pokud potřebujeme, můžeme typový parametr explicitně uvést, a tím požadované chování vynutit.

ParametricType{Float64}(2)
ParametricType{Float64}(2.0)

ParametricType{T} je podtypem ParametricType. Pokud je T podtypem S, pak ParametricType{T} není podtypem ParametricType{S}!

ParametricType{Int64} <: ParametricType
true
Int64 <: Integer
true
ParametricType{Int64} <: ParametricType{Integer}
false

Ve výše uvedené definici typu ParametricType{T} není typ T nijak omezen, může být zcela libolný. To je často nevhodné. Pomocí operátoru <: můžeme zafixovat supertyp typu T:

struct ParametricType2{T <: Number}
    a::T
end

Následující typy a jejich instance poté přicházejí v úvahu:

ParametricType2{Float64}
ParametricType2{Float64}
ParametricType2(10)
ParametricType2{Int64}(10)
ParametricType(1im)
ParametricType{Complex{Int64}}(0 + 1im)

Následující typy a jejich instance už ale nejsou validní, uvedené typy už nejsou podtypy typu Number.

ParametricType2{Array}
TypeError: in ParametricType2, in T, expected T<:Number, got Type{Array}

Stacktrace:
 [1] top-level scope
   @ In[55]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
ParametricType2("Hello!")
MethodError: no method matching ParametricType2(::String)
The type `ParametricType2` exists, but no method is defined for this combination of argument types when trying to construct it.

Closest candidates are:
  ParametricType2(::T) where T<:Number
   @ Main In[51]:2


Stacktrace:
 [1] top-level scope
   @ In[56]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
ParametricType2{String}("Hi!")
TypeError: in ParametricType2, in T, expected T<:Number, got Type{String}

Stacktrace:
 [1] top-level scope
   @ In[57]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Parametrické typy samozřejmě mohou mít více typových parametrů, stačí je oddělit čárkami. Například:

struct ExampleType{T <: Number, S <: String}
    a::T
    b::S
end
ex = ExampleType(1, "a")
ExampleType{Int64, String}(1, "a")
ex.a
1
ex.b
"a"

Typový parametr nemusí být nutně typ – může jím být i hodnota (typicky celé číslo). Typickým příkladem využití této možnosti jsou různé typy polí, například matice (všimněte si dvojky!):

Matrix
Matrix (alias for Array{T, 2} where T)

Jako hodnota parametrického typu může sloužit i symbol. Toho se v Julia využívá třeba pro vyjádření matematických konstant:

typeof(π)
Irrational{:π}
typeof(ℯ)
Irrational{:ℯ}
Irrational{:π}()
π = 3.1415926535897...
Irrational{:ℯ}()
ℯ = 2.7182818284590...

2.6 Sjednocení typů a aliasy

Přirozeně můžeme vytvářet typy reprezentující sjednocení dvou a více typů. Máme-li typy S a T, pak instance Union{T, S} mohou být typu T nebo S. Dále je možné pro komplikovanější typy (například velká sjednocení) vytvářet aliasy pomocí jednoduchého přiřazení.

Ukažme si oba tyto koncepty na jednoduchém příkladě:

const Numeric = Union{Int64, Float64}
Union{Float64, Int64}
Numeric
Union{Float64, Int64}
Float64 <: Numeric
true
Int64 <: Numeric
true
String <: Numeric
false
Numeric(1)
1
typeof(Numeric(1))
Int64
Numeric(1.0)
1.0
typeof(Numeric(1.0))
Float64

2.7 Cvičení / Příklad Q\mathbb{Q}

Jako zcela elementární příklad si ukažme vlastní implementaci typu modelujícího množinu racionálních čísel. K tomuto příkladu se vrátíme i v budoucí lekci.

Zopakujme, že objekt daného typu vytvoříme pomocí konstruktoru. Výchozí konstruktor má stejné jméno jako typ a bere popořadě jako argumenty jednotlivé atributy složeného typu. Často potřebujeme definovat vlastní konstruktor, který třeba na základě zadaných hodnot dopočítá další atributy, nebo provede nějakou jejich úpravu či kontrolu.

K tomu máme dvě možnosti, první jsou vnitřní konstruktory, uvedené v těle struct (případně mutable struct), které nelze později měnit a lze tak pomocí nich například vynutit podmínky, které musí objekty našeho typu splňovat. Všimněte si speciální metody new, na kterou lze pohlížet jako na výchozí konstruktor.

Omezení T <: Integer za jménem typu je zkrácený zápis klauzule where {T <: Integer}. Druhý zápis je nutný tehdy, chceme-li se na typový parametr T odkazovat uvnitř definice metody (jako ve vnitřním konstruktoru níže).

struct MyRational{T <: Integer} <: Number
    num::T
    den::T

    function MyRational(num::S, den::S) where { S <: Integer }
        if den == S(0)
            error("Zero denominator is forbidden by god (or nature)!")
        end
        
        # divide by common factors
        common = gcd(num, den)
        
        return new{S}(div(num, common), div(den, common))
    end
end
MyRational <: Number
true
struct MyRational2{T <: Integer} # bez <: Number
    num::T
    den::T
end
MyRational2 <: Number
false

Naše první "racionální číslo":

q = MyRational(10, 2)
q
MyRational{Int64}(5, 1)

Nulou nepodělíš!

MyRational(1, 0)
Zero denominator is forbidden by god (or nature)!

Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:44
 [2] MyRational(num::Int64, den::Int64)
   @ Main ./In[77]:7
 [3] top-level scope
   @ In[82]:1
 [4] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
MyRational2(1, 0)
MyRational2{Int64}(1, 0)

Všimněte si rozdílu: MyRational2 nemá žádnou kontrolu a nulový jmenovatel se tiše zkonstruuje. Kontrolu vynucenou vnitřním konstruktorem má až MyRational.

Dále (i později po definici typu) můžeme definovat vnější konstruktory, což jsou obyčejné metody, které použijí některý z vniřních konstruktorů.

# V podstatě kanonické vnoření celých čísel do racionálních.
MyRational(n::T) where { T <: Integer } = MyRational(n, T(1))

MyRational(4)
MyRational{Int64}(4, 1)
methods(MyRational)
# 5 methods for type constructor:
  • MyRational(n::T) where T<:Integer in Main at In[84]:2
  • MyRational(num::S, den::S) where S<:Integer in Main at In[77]:5
  • (::Type{T})(x::Base.TwicePrecision) where T<:Number in Base at twiceprecision.jl:265
  • (::Type{T})(x::T) where T<:Number in Core at boot.jl:1018
  • (::Type{T})(x::AbstractChar) where T<:Union{AbstractChar, Number} in Base at char.jl:52

Na tomto místě znovu zdůrazněme, že objekty takto zavedeného typu nejsou měnitelné (jsou immutable):

println(q.num)
q.num = 10
5
setfield!: immutable struct of type MyRational cannot be changed

Stacktrace:
 [1] setproperty!(x::MyRational{Int64}, f::Symbol, v::Int64)
   @ Base ./Base_compiler.jl:61
 [2] top-level scope
   @ In[86]:2
 [3] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Přirozeně se nabízí otázka, jak zadefinovat algebraické operace mezi těmito racionálními čísly. Tím se přesně budeme zabývat v přístí lekci.


2.8 Výpis desítkové reprezentace

Pro strojová čísla lze vypsat jejich surovou binární reprezentaci (metoda bitstring), která ale není asi příliš užitečná. K dispozici ovšem máme modul Printf a jeho makro @printf, které podporuje známé formátovaní z C++.

using Printf
@printf "%.100f" 1/3
0.3333333333333333148296162562473909929394721984863281250000000000000000000000000000000000000000000000

Řešení některých příkladů

Poznámka k příkladu s absolutní hodnotou.

my_abs(x) = x >= 0 ? x : -x

Poznámka k příkladu s řetězovými zlomky.

function my_cf(seq)
    value = seq[end]
    for k = (length(seq)-1):-1:1
        value = seq[k] + 1 / value
    end
    
    return value
end

Reference

Další detaily můžete nalézt zejména v kapitolách Control Flow, Types a Constructors dokumentace Julia.

Upozorňujeme studenty na zajímavou stránku OEIS.