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 + 12
#= 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
123123
1_000_0001000000
1_234.123_4561234.123456
# Float
12.212.2
# Float
12.012.0
Nebo, pro minimalisty:
# Float
.10.1
# Float
1.1.0
11
Dále je k dispozici tzv. "vědecký" tvar strojového čísla:
# Float
1.23e-101.23e-10
1e-101.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 + 5im10 + 5im
# Complex number with float real and imaginary parts
3. + 1im3.0 + 1.0im
0im0 + 0im
1im0 + 1im
im má význam i samo o sobě jako imaginární jednotka:
imim
Ř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:489Jednoduché 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 ^ 8256
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.
x5UndefVarError: `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. * x50.0
Ale pokus o vyhodnocení následující buňky skončí chybou.
5.xParseError: # 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.0x50.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.
4yUndefVarError: `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 + 410
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 ==
falsefalse
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 / 22.0
Následující hodnota není rovna :
1 / 30.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 \ 33.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 \ 36.0
(1 / 2) \ 36.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 ÷ 22
2 ÷ 40
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 % 32
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 // 22//1
1 // 3 + 2 // 511//15
1 // 3 * 6 // 52//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_10.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
5Pozor! Indexujeme od (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:489a[-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:489a[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.0Nebo 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:489Následující kód vytvoří čtvercovou matici.
A = [1 2; 3 4]2×2 Matrix{Int64}:
1 2
3 4Pokud 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 4Bě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 4hcat([1 2], [3 4])1×4 Matrix{Int64}:
1 2 3 4Pozor na rozměry (mezery vs. čárky):
vcat([1, 2], [3, 4])4-element Vector{Int64}:
1
2
3
4hcat([1, 2], [3, 4])2×2 Matrix{Int64}:
1 3
2 4Tí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 4Pole 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.
A2×2 Matrix{Int64}:
1 2
3 4A[1, 2] = 999 # první řádek, druhý sloupec
A2×2 Matrix{Int64}:
1 999
3 4A[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:489A[1, 1] = 1.1InexactError: 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:489A[1, 1] = 5.05.0
A2×2 Matrix{Int64}:
5 999
3 4Tuple (tento termín do češtiny překládat nebudeme -- nejblíže by k němu měla "uspořádaná -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 11
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 . Prvky tuple nelze modifikovat.
t[1]1
t[1] = 10MethodError: 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:489Oproti tomu:
a = [1, 2]
a[1] = 10
a2-element Vector{Int64}:
10
2"NamedTuple"
t = (a = 1, b = 2)(a = 1, b = 2)
t[1]1
t[:b]2
t[:c] = 10MethodError: 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:489Poslední 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?"
dDict{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
d2Dict{Any, Any} with 2 entries:
2 => "Ahoj!"
"a" => 0+42imPozor:
d2[2.0] = "Ha!"
d2Dict{Any, Any} with 2 entries:
2.0 => "Ha!"
"a" => 0+42imd2[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:489f([1, 2], [3, 4])2-element Vector{Int64}:
4
6g(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 |> sin0.8414709848078965
sin(1.0)0.8414709848078965
1.0 |> sin |>
x -> x^2 |>
cos0.7596162788336037
Tuto operaci lze řetězit, případně i rozepsat pro přehlednost na více řádků.
2 |> x -> x^2 |> sqrt2.0
Pozor, známý symbol | má význam bitového OR.
1 | 23
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 + 21 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 # \approxtrue
√4 # \sqrt2.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 ⊕ 30
4 ⊕ 32
0.8 Konstanty
Konstanty definujeme pomocí klíčového slova const.
const AlmostPi = 3.03.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 = 1invalid 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
AlmostPi3.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 = 22
a1
a + b;Cvičení
- Výše jsme viděli, že číslo odpovídá Julia konstantě se stejným symbolem. Jak je to s Eulerovým číslem?
- Pod jakými názvy najdete v Julia trigonometrické funkce, exponenciální funkce a logaritmus?
- Napadá vás nějaký základní konstrukt, vám dobře známý z jiných programovacích jazyků, který jsme nezmínili?
- 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.01.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!")
endTypeError: 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
Definujte odpovídající metodu my_abs v Julia.
my_abs(x) = x < 0 ? -x : xmy_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 / 0false
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!
nothingHodnota 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 && nothingfalse || nothingAlternativně 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 & 22 | 12 ⊻ 3A dělení nulou nám projde i teď :-D.
true & 1 / 01 / 0Dě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 // 0Float64(1 // 0)Klíčová slova and nebo or zde neexistují.
true and trueand1.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
zNebo 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)
z1.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)
end1 2 3
1:31:3
V rozsahu lze případně i měnit délku kroku.
for j in 1:2:6
println(j)
end1 3 5
Můžeme pak snadno iterovat i v opačném pořadí.
for j = 5:-1:0
println(j)
end5 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))
endInt64
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)
end2 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)
end1 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.00.1:0.1:6.0
[x for x in 0.1:0.5:6.0] # k této syntaxi se ještě dostaneme12-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.6Cvičení: Naivní test prvočíselnosti
Implementujte "naivní" test prvočíselnosti: Má-li přirozené číslo netriviálního dělitele, pak tento nutně leží mezi a (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
endPouž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)
endsqrt(4)Cvičení: Řetězové zlomky
Mějme -tici . Následující výraz nazýváme (ukončeným) řetězovým zlomkem
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
endmy_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
ifabeginnevytvářejí nový rozsah. - Naopak těla cyklů (
for,while), blokytry-catch, funkce a blokyletvytvář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` neexistuje1 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
totalJasně 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ěnila1.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)
endtry
sqrt(-1) # odmocnina ze záporného čísla
catch e
println(typeof(e), ": ", e)
endVlastní 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)
endBlok 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")
endNa 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 <: Numbertrue
Real <: Anytrue
Any <: Anytrue
Nothing <: Anytrue
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 <: Complexfalse
Complex <: Numbertrue
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 <: Realtrue
Int64 <: AbstractFloatfalse
Int64 <: Integertrue
Přirozeně se nabízí otázka, jakého typu je typ?
typeof(Int64)DataType
typeof(DataType)DataType
DataType <: Anytrue
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
Float64subtypes(Signed)6-element Vector{Any}:
BigInt
Int128
Int16
Int32
Int64
Int8subtypes(Integer)3-element Vector{Any}:
Bool
Signed
Unsignedsubtypes(Unsigned)5-element Vector{Any}:
UInt128
UInt16
UInt32
UInt64
UInt82.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 Int643
x = 2.0
(x + 2)::Int64 # součet by sice přirozeně šlo převést na Int64, ale není to Int64TypeError: 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řevodTypeError: 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)::Integer6
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
endF (generic function with 1 method)
F(2) # aritmetika probíhá v Int6417
F(0.5) # výsledek lze přirozeně převést na Int642
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
endg (generic function with 1 method)
g(10)20
g(4.0)8
Všimněte si, že:
2 * 4.08.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
endtypeof(ImmutableType)DataType
supertypes(ImmutableType)(ImmutableType, Any)
struct MyNumber <: Number
a::Integer
endsupertypes(MyNumber)(MyNumber, Number, Any)
MyNumber <: Numbertrue
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
xImmutableType(1)
x = MutableType(1, "Ahoj!")
println(x.a)
println(x.b)
x.a = 10
println(x.a)1 Ahoj! 10
xMutableType(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:489MutableType(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
endVý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.a10
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} <: ParametricTypetrue
Int64 <: Integertrue
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
endNá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:489ParametricType2("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:489Parametrické 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
endex = ExampleType(1, "a")ExampleType{Int64, String}(1, "a")ex.a1
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!):
MatrixMatrix (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}NumericUnion{Float64, Int64}Float64 <: Numerictrue
Int64 <: Numerictrue
String <: Numericfalse
Numeric(1)1
typeof(Numeric(1))Int64
Numeric(1.0)1.0
typeof(Numeric(1.0))Float64
2.7 Cvičení / Příklad
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
endMyRational <: Numbertrue
struct MyRational2{T <: Integer} # bez <: Number
num::T
den::T
endMyRational2 <: Numberfalse
Naše první "racionální číslo":
q = MyRational(10, 2)
qMyRational{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)- 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 = 105
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:489Př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/30.3333333333333333148296162562473909929394721984863281250000000000000000000000000000000000000000000000
Řešení některých příkladů
Poznámka k příkladu s absolutní hodnotou.
my_abs(x) = x >= 0 ? x : -xPozná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
endReference
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.