Jdi na navigaci předmětu

04 Funkce, metody, makra a metaprogramování

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 2026/2027 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)

1. Funkce a metody

V předchozí kapitole jsme si ukázali, jak se v Julii pracuje s typy. Asi jste si všimli, že na typy v Julii a jejich hierarchii se lze dívat trochu jako na třídy v jiných programovacích jazycích.

Na rozdíl ale třeba od C++, Javy, Ruby, nebo Pythonu, nejsou instance tříd (objekty) vybaveny metodami. Koncept funkcí a metod je v Julii prakticky zcela oddělen od typů.


1.1 Multiple dispatch

V matematice se jeden symbol ++ používá k označení různých binárních operací. Všechny tyto operace mají jisté společné vlastnosti, které umožňují dívat se na danou operaci jako na sčítání. Tento abstraktní koncept sčítání by v Julii odpovídal funkci. Různé konkrétní způsoby sčítání objektů (nejen) stejných typů (čísla, matice,...) pak v Julii odpovídají metodám.

Jinak řečeno, Julia funkce pod jedním společným jménem sdružuje více metod, které se při volání použijí v závislosti na typech argumentů. Toto paradigma se označuje multiple dispatch (v kontrastu k single dispatch, kde volání je vázáno na konkrétní třídu).

Například zmíněná funkce + má v Julii následující metody (lehce extrémní příklad, ale názorný):

methods(+)
# 190 methods for generic function + from Base:

Každá z těchto 190 možností představuje konkrétní implementaci sčítání pro argumenty uvedené v signatuře. Zvídavý uživatel může jedním prostým kliknutím rovnou nahlédnout zdrojový kód.


Pozor na ne/jednoznačnost!

Při volání metody se Julia snaží vždy vybrat tu "nejspecifičtější" vzhledem k typovému systému. Například:

f(x::Integer) = 2 * x
f(x::Number) = 3 * x
f (generic function with 2 methods)
f(1) # 1 "je" jak Integer tak Number a Integer <: Number
2
Int64 <: Number
true
Int64 <: Integer
true
Integer <: Number
true
f(1.0) # 1.0 "je" jen Number
3.0

Samozřejmě hrozí nebezpečí nejednoznačnosti. Na takovou situaci nás naštěstí Julia upozorní.

f(x::Float64, y) = x + y
f(x, y::Float64) = x * y
f (generic function with 4 methods)
f(1.0, 2)
3.0
f(1, 2.0)
2.0
@which f(1.0, 2)
f(x::Float64, y) in Main at In[9]:1
@which f(1, 2.0)
f(x, y::Float64) in Main at In[9]:2
f(1.0, 2.0)
MethodError: f(::Float64, ::Float64) is ambiguous.

Candidates:
  f(x, y::Float64)
    @ Main In[9]:2
  f(x::Float64, y)
    @ Main In[9]:1

Possible fix, define
  f(::Float64, ::Float64)


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

Funkce f má aktuálně čtyři metody:

methods(f)
# 4 methods for generic function f from Main:
  • f(x, y::Float64) in Main at In[9]:2
  • f(x::Float64, y) in Main at In[9]:1
  • f(x::Integer) in Main at In[3]:1
  • f(x::Number) in Main at In[3]:2

Možná oprava spočívá v dodefinování chybějící metody:

f(x::Float64, y::Float64) = x - y
f (generic function with 5 methods)
f(1.0, 2.0)
-1.0
methods(f)
# 5 methods for generic function f from Main:
  • f(x::Float64, y::Float64) in Main at In[16]:1
  • f(x, y::Float64) in Main at In[9]:2
  • f(x::Float64, y) in Main at In[9]:1
  • f(x::Integer) in Main at In[3]:1
  • f(x::Number) in Main at In[3]:2
f(1, 2.0)

1.2 Definice metod: parametrické metody

Podobně jako typy mohly mít parametry, mohou mít parametry i metody. Můžeme tak mít různé varianty metody v závislosti na jejich argumentech.

function same_type(x::T, y::T) where {T}
    println(T)
    return true
end

function same_type(x, y)
    return false
end
same_type (generic function with 2 methods)

Díky anotaci se první metoda použije pouze v případě, že jsou argumenty stejného typu. Pokud nejsou, zavolá se druhá "obecná" metoda.

same_type(1, 2)
Int64
true
same_type(1.0, 2.0)
Float64
true
same_type(1, 2.0)
false
same_type(Int64, String)
DataType
true
methods(same_type)
# 2 methods for generic function same_type from Main:
  • same_type(x::T, y::T) where T in Main at In[20]:1
  • same_type(x, y) in Main at In[20]:6

Dále můžeme v anotaci omezit i samotný typ T, nebo použít více typů:

function parametric_f(x::T, y::T, z::S) where { T <: Number, S <: AbstractString }
    return nothing
end
parametric_f (generic function with 1 method)

Tato metoda vyžaduje, aby první dva argumenty byly stejného "číselného" typu T a aby poslední argument byl podtypem typu AbstractString.


1.3 Příklady a cvičení

V předchozí lekci jsme definovali vlastní typ pro racionální čísla. Pojďme nyní zadefinovat sčítání a další operace i pro ně.

"""

    MyRational{T <: Integer} <: Number

_My_ **home** made rational number. Kód `MyRational`, LaTeX ``\\frac{1}{2}``.
"""
struct MyRational{T <: Integer} <: Number
    num::T
    den::T
    
    function MyRational(num::T, den::T) where { T <: Integer }
        den == 0 && error("Zero denominator is forbidden by god!")
        if den < 0
            num *= -1
            den *= -1
        end
        
        # divide by common factors
        common = gcd(num, den)
        new{T}(div(num, common), div(den, common))
    end
end

MyRational{T}(z::T) where { T <: Integer } = MyRational(z, one(T))
MyRational{Int64}(1)
\begin{equation*}1\end{equation*}

Použili jsme ještě jednu novinku a tou je docstring před definicí typu. Tímto způsobem ho můžete použít i před definicí metod, typů, maker. Později k němu můžete přistupovat pomocí integrované nápovědy, nebo ho použít při generování dokumentace (touto problematikou se budeme zabývat později během semestru). Všimněte si, že v docstringu můžeme používat Markdown.

?MyRational
search: MyRational Rational Irrational

MyRational{T <: Integer} <: Number

My home made rational number. Kód MyRational, LaTeX 12\frac{1}{2}.

Pokud chceme definovat novou metodu funkce, která je definována externě (v jiném modulu, o nich později), musíme ji explicitně importovat nebo musíme uvést celé její jméno. První možnost:

p = MyRational(1, 2)
q = MyRational(2, 3)

p + q
+ not defined for MyRational{Int64}

Stacktrace:
 [1] error(::String, ::String, ::Type)
   @ Base ./error.jl:54
 [2] no_op_err(name::String, T::Type)
   @ Base ./promotion.jl:626
 [3] +(x::MyRational{Int64}, y::MyRational{Int64})
   @ Base ./promotion.jl:627
 [4] top-level scope
   @ In[3]:4
 [5] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
import Base.+

+(p::MyRational{T}, q::MyRational{T}) where { T <: Integer } =
    MyRational(p.num * q.den + q.num * p.den, p.den * q.den)
+ (generic function with 191 methods)

Pojďme ji hned s nadšením otestovat.

p = MyRational(1, 2)
q = MyRational(2, 3)

p + q
MyRational{Int64}(7, 6)

Toto je sice správný výsledek, ale esteticky není uspokojivý. Více oku lahodící vypisování našich racionálních čísel můžeme zajistit pomocí přidání metody k funkci show (viz Custom Pretty-printing):

# bez explicitního `import Base.show`

Base.show(io::IO, q::MyRational{T}) where { T <: Integer } =
    print(io, q.num, "/", q.den)

Potom dostaneme:

p + q
7/6
MyRational(2, -3)
-2/3
MyRational(2, 1)
2/1

Pokud chceme hezký LaTeX výstup, pak musíme dodefinovat následující metodu.

function Base.show(io::IO, ::MIME"text/latex", q::MyRational{T}) where { T <: Integer }
    if q.den == one(T)
        print(io, "\\begin{equation*}$(q.num)\\end{equation*}")
    elseif q.num < zero(T)
        print(io, "\\begin{equation*}-\\frac{$(-q.num)}{$(q.den)}\\end{equation*}")
    else
        print(io, "\\begin{equation*}\\frac{$(q.num)}{$(q.den)}\\end{equation*}")
    end
end
MyRational(25, 100)
\begin{equation*}\frac{1}{4}\end{equation*}
MyRational(-24, 40)
\begin{equation*}-\frac{3}{5}\end{equation*}
MyRational(0, 1)
\begin{equation*}0\end{equation*}
MyRational(2, 2)
\begin{equation*}1\end{equation*}
MyRational(-1, 1)
\begin{equation*}-1\end{equation*}

Pro úplnost můžeme jednoduše definovat násobení (binární operátor *) a opačný prvek (unární operátor -) a odčítání (binární operátor -). Všimněte si, jak v definici odčítání -- pěkně v souladu s matematickou definicí -- použijeme pouze sčítání a definici opačného prvku.

import Base.-, Base.*

*(p::MyRational{T}, q::MyRational{T}) where { T <: Integer } = MyRational(p.num * q.num, p.den * q.den)
-(p::MyRational{T}) where { T <: Integer}                    = MyRational(-one(T), one(T)) * p
# nebo: = MyRational(-p.num, p.den)
-(p::MyRational{T}, q::MyRational{T}) where { T <: Integer } = p + (-q)
- (generic function with 196 methods)

Otestujme správnou funkčnost algebraických operací mezi našimi racionálními čísly:

println("p = ", p, ", q = ", q)

p * q
p = 1/2, q = 2/3
\begin{equation*}\frac{1}{3}\end{equation*}
-p
\begin{equation*}-\frac{1}{2}\end{equation*}
p - q
\begin{equation*}-\frac{1}{6}\end{equation*}
p ^ 5
\begin{equation*}\frac{1}{32}\end{equation*}
@which p ^ 5
literal_pow(f::typeof(^), x, ::Val{p}) where p in Base at intfuncs.jl:478
p ^ (-2)
/ not defined for MyRational{Int64}

Stacktrace:
 [1] error(::String, ::String, ::Type)
   @ Base ./error.jl:54
 [2] no_op_err(name::String, T::Type)
   @ Base ./promotion.jl:626
 [3] /(x::MyRational{Int64}, y::MyRational{Int64})
   @ Base ./promotion.jl:630
 [4] inv(x::MyRational{Int64})
   @ Base ./number.jl:299
 [5] literal_pow(f::typeof(^), x::MyRational{Int64}, ::Val{-2})
   @ Base ./intfuncs.jl:483
 [6] top-level scope
   @ In[32]:1
 [7] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
1 / p
promotion of types Int64 and MyRational{Int64} failed to change any arguments

Stacktrace:
 [1] error(::String, ::String, ::String)
   @ Base ./error.jl:54
 [2] sametype_error(input::Tuple{Int64, MyRational{Int64}})
   @ Base ./promotion.jl:428
 [3] not_sametype(x::Tuple{Int64, MyRational{Int64}}, y::Tuple{Int64, MyRational{Int64}})
   @ Base ./promotion.jl:422
 [4] promote
   @ ./promotion.jl:405 [inlined]
 [5] /(x::Int64, y::MyRational{Int64})
   @ Base ./promotion.jl:436
 [6] top-level scope
   @ In[33]:1
 [7] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Cvičení: Inverze a mocnění v Q\mathbb{Q}

Naše racionální čísla bychom ještě chtěli umocňovat, i na záporné exponenty (speciálně na −1-1, tedy invertovat). Dokážete vyřešit jak na to?

p ^ 3
\begin{equation*}\frac{1}{8}\end{equation*}
@which p ^ 3
literal_pow(f::typeof(^), x, ::Val{p}) where p in Base at intfuncs.jl:478
p ^ (-1)
/ not defined for MyRational{Int64}

Stacktrace:
 [1] error(::String, ::String, ::Type)
   @ Base ./error.jl:54
 [2] no_op_err(name::String, T::Type)
   @ Base ./promotion.jl:626
 [3] /(x::MyRational{Int64}, y::MyRational{Int64})
   @ Base ./promotion.jl:630
 [4] inv(x::MyRational{Int64})
   @ Base ./number.jl:299
 [5] literal_pow(f::typeof(^), x::MyRational{Int64}, ::Val{-1})
   @ Base ./intfuncs.jl:483
 [6] top-level scope
   @ In[36]:1
 [7] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
MyRational{T}(n::T) where { T <: Integer } = MyRational(n, one(T))
@which one(1)
one(x::T) where T<:Number in Base at number.jl:400
p ^ (-1)
/ not defined for MyRational{Int64}

Stacktrace:
 [1] error(::String, ::String, ::Type)
   @ Base ./error.jl:54
 [2] no_op_err(name::String, T::Type)
   @ Base ./promotion.jl:626
 [3] /(x::MyRational{Int64}, y::MyRational{Int64})
   @ Base ./promotion.jl:630
 [4] inv(x::MyRational{Int64})
   @ Base ./number.jl:299
 [5] literal_pow(f::typeof(^), x::MyRational{Int64}, ::Val{-1})
   @ Base ./intfuncs.jl:483
 [6] top-level scope
   @ In[39]:1
 [7] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
Base.:/(p::MyRational{T}, q::MyRational{T}) where { T <: Integer } = MyRational(p.num * q.den, p.den * q.num)
p
\begin{equation*}\frac{1}{2}\end{equation*}
p ^ (-1)
\begin{equation*}2\end{equation*}
MyRational(1, 0) / MyRational(0, 1)
Zero denominator is forbidden by god!

Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:44
 [2] MyRational(num::Int64, den::Int64)
   @ Main ./In[29]:12
 [3] top-level scope
   @ In[43]:1
 [4] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
Base.inv(q::MyRational{T}) where { T <: Integer } = MyRational(q.den, q.num)
inv(MyRational(3, 5))
\begin{equation*}\frac{5}{3}\end{equation*}
-inv(MyRational(-3, 5))
\begin{equation*}\frac{5}{3}\end{equation*}
p ^ (-2)
\begin{equation*}4\end{equation*}
p ^ (-1)
\begin{equation*}2\end{equation*}
inv(MyRational(0, 1))
Zero denominator is forbidden by god!

Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:44
 [2] MyRational(num::Int64, den::Int64)
   @ Main ./In[29]:12
 [3] inv(q::MyRational{Int64})
   @ Main ./In[44]:1
 [4] top-level scope
   @ In[49]:1
 [5] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
p
\begin{equation*}\frac{1}{2}\end{equation*}
1 + p
promotion of types Int64 and MyRational{Int64} failed to change any arguments

Stacktrace:
 [1] error(::String, ::String, ::String)
   @ Base ./error.jl:54
 [2] sametype_error(input::Tuple{Int64, MyRational{Int64}})
   @ Base ./promotion.jl:428
 [3] not_sametype(x::Tuple{Int64, MyRational{Int64}}, y::Tuple{Int64, MyRational{Int64}})
   @ Base ./promotion.jl:422
 [4] promote
   @ ./promotion.jl:405 [inlined]
 [5] +(x::Int64, y::MyRational{Int64})
   @ Base ./promotion.jl:433
 [6] top-level scope
   @ In[51]:1
 [7] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
MyRational(1, 2) + MyRational(BigInt(1), BigInt(2))
promotion of types MyRational{Int64} and MyRational{BigInt} failed to change any arguments

Stacktrace:
 [1] error(::String, ::String, ::String)
   @ Base ./error.jl:54
 [2] sametype_error(input::Tuple{MyRational{Int64}, MyRational{BigInt}})
   @ Base ./promotion.jl:428
 [3] not_sametype(x::Tuple{MyRational{Int64}, MyRational{BigInt}}, y::Tuple{MyRational{Int64}, MyRational{BigInt}})
   @ Base ./promotion.jl:422
 [4] promote
   @ ./promotion.jl:405 [inlined]
 [5] +(x::MyRational{Int64}, y::MyRational{BigInt})
   @ Base ./promotion.jl:433
 [6] top-level scope
   @ In[52]:1
 [7] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Racionální čísla jsou v Julii k dispozici i bez našeho typu jako typ Rational{T}. Jako konstruktor můžeme využít i dvojité lomítko:

2 // 3
2//3
4 // 2
2//1
1 + 2 // 3
5//3
q = 3 // 2
3//2

Poznámka: Hezčí výpis.

function Base.show(io::IO, ::MIME"text/latex", q::Rational{T}) where { T <: Integer }
    if q.num < 0
        print(io, "\\begin{equation*}-\\frac{$(-q.num)}{$(q.den)}\\end{equation*}")
    elseif q.num == 0
        print(io, "\\begin{equation*}0\\end{equation*}")
    else
        print(io, "\\begin{equation*}\\frac{$(q.num)}{$(q.den)}\\end{equation*}")
    end
end
33 // 12
\begin{equation*}\frac{11}{4}\end{equation*}

Pozor na type piracy: právě jsme přidali metodu Base.show(..., ::MIME"text/latex", ::Rational), tedy jsme rozšířili chování cizího typu (Rational z Base). Takový zásah mění chování i všem ostatním v programu a obecně se mu vyhýbáme. U vlastních typů (jako MyRational) je to naopak v pořádku.

2 // 3
\begin{equation*}\frac{2}{3}\end{equation*}
@which Rational(1, 2)
Rational(n::T, d::T) where T<:Integer in Base at rational.jl:48

Poznámka: Julia Rational umí vytvořit exaktní reprezentaci strojového čísla (které z definice je vždy racionální číslo):

x = Rational(0.3)
\begin{equation*}\frac{5404319552844595}{18014398509481984}\end{equation*}
@which Rational(0.3)
Rational(x::Float64) in Base at rational.jl:180
x.num / x.den
0.3
Rational(0.3) - 3 // 10
\begin{equation*}-\frac{1}{90071992547409920}\end{equation*}
Float64(Rational(0.3) - 3 // 10)
-1.1102230246251566e-17
Rational(0.5)
\begin{equation*}\frac{1}{2}\end{equation*}

Cvičení: Komplexní čísla

Vytvořte vlastní typ MyComplex{T} modelující komplexní čísla a vybavte ho standardními metodami sčítání +, odčítání -, násobení *, dělení / a inverze inv.

struct MyComplex{T <: Real} <: Number
    re::T
    im::T
end

Base.show(io::IO, z::MyComplex{T}) where { T <: Real } =
    if z.im >= 0
        print(io, z.re, " + ", z.im, "ı")
    else
        print(io, z.re, " - ", -z.im, "ı")
    end
z = MyComplex(1, 2)
1 + 2ı
z = MyComplex(-1, 2)
-1 + 2ı
z = MyComplex(1, -2)
1 - 2ı
import Base.*, Base.+, Base.-, Base.inv, Base./

*(u::MyComplex{S}, v::MyComplex{T}) where {S, T} = nothing # FIX ME

+(u::MyComplex{S}, v::MyComplex{T}) where {S, T} = nothing

-(u::MyComplex{T}) where {T} = nothing

-(u::MyComplex{S}, v::MyComplex{T}) where {S, T} = nothing

function inv(u::MyComplex{T}) where {T}
    # ...
    return nothing
end

/(u::MyComplex{S}, v::MyComplex{T}) where {S, T} = nothing
w = MyComplex(3, 4)
z
z + w
z * w
inv(z) * MyComplex(1.0, -2.0)
MyComplex(1.0, -2.0) / MyComplex(1.0, -2.0)
z / z

Cvičení: Modulární multiplikativní grupa

Pro prvočíslo p∈Np \in \mathbb{N} tvoří množina {1,2,…,p−1}\{1,2,\ldots,p-1\} s operací násobení modulo pp grupu (nula mezi prvky nepatří – nemá inverzi; tzv. modulární multiplikativní grupa, značí se Zp×\mathbb{Z}_p^{\times} nebo GF(p)∗GF(p)^*). O pp pak mluvíme jako o modulu.

Vytvořte v Julii typ, který bude parametrizovaný typem integeru a modulem pp, a bude modelovat výše uvedenou strukturu. Vhodně zadefinujte operaci *, inv a zajistěte pěkný výpis objektů tohoto typu. Definujte metodu modulus vracící modul. Doplňte následující šablonu:

Poznámka: k výpočtu isprime používáme balíček Primes. Pokud ho ještě nemáte, nainstalujte si ho, např. pomocí balíčkovacího módu, nebo přímo zde pomocí import Pkg; Pkg.add("Primes").

import Base.*, Base.inv, Base.show
using Primes

"""
Modular Multiplicative Group (MMG).
"""
struct MMG{T <: Integer, P} <: Number
    value::T
    
    function MMG(value::T, modulus::T) where { T <: Integer }
        isprime(modulus) || error("modulus ($modulus) has to be prime!")

        my_value = mod(value, modulus)
        iszero(my_value) && error("0 is not acceptable!")
        
        new{T, modulus}(my_value)
    end
end

modulus(u::MMG{T, P}) where { T <: Integer, P } = P

function *(a::MMG{T, P}, b::MMG{T, P}) where { T <: Integer, P }
    MMG(mod(a.value * b.value, P), P)
end

function inv(a::MMG{T, P}) where { T <: Integer, P }
    _, u, _ = gcdx(a.value, P)
    MMG(mod(u, P), P)
end

show(io::IO, a::MMG) = print(io, "|", a.value, "|_$(modulus(a))")
show (generic function with 282 methods)

Následuje několik ukázek použití.

a = MMG(3, 11); b = MMG(5, 11); c = MMG(5, 13);
a
|3|_11
b
|5|_11
c
|5|_13
typeof(a)
MMG{Int64, 11}
modulus(a)
11
a * b
|4|_11
# toto by šlo ještě vylepšit..., výchozí chování.
b * c
promotion of types MMG{Int64, 11} and MMG{Int64, 13} failed to change any arguments

Stacktrace:
 [1] error(::String, ::String, ::String)
   @ Base ./error.jl:54
 [2] sametype_error(input::Tuple{MMG{Int64, 11}, MMG{Int64, 13}})
   @ Base ./promotion.jl:428
 [3] not_sametype(x::Tuple{MMG{Int64, 11}, MMG{Int64, 13}}, y::Tuple{MMG{Int64, 11}, MMG{Int64, 13}})
   @ Base ./promotion.jl:422
 [4] promote
   @ ./promotion.jl:405 [inlined]
 [5] *(x::MMG{Int64, 11}, y::MMG{Int64, 13})
   @ Base ./promotion.jl:434
 [6] top-level scope
   @ In[81]:2
 [7] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
a
|3|_11
inv(a)
|4|_11
a * inv(a)
|1|_11
c * inv(c)
|1|_13
inv(c)
|8|_13
c
|5|_13
b ^ 123
|4|_11
MMG(0, 3)
0 is not acceptable!

Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:44
 [2] MMG(value::Int64, modulus::Int64)
   @ Main ./In[72]:14
 [3] top-level scope
   @ In[89]:1
 [4] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

1.4 Předávání argumentů metodám

Už víme, jak v definici metody anotovat typy argumentů a návratové hodnoty. Pro připomenutí:

F(x::Int64)::Float64 = x / (abs(x) + 1)
F (generic function with 1 method)
F(1)
0.5
F(0)
0.0

Ovšem:

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

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


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

Asi je jasné, jak definovat metodu s více pozičními argumenty. V této části lekce si ukážeme, jak argumentům přiřazovat výchozí hodnoty, jak definovat metody s proměnlivým počtem argumentů, či jak definovat metody s argumenty ve tvaru keyword=value, u nichž nezáleží na pořadí.

U všech níže uvedených způsobů předání argumentů lze uvést i typovou anotaci.


Nepovinné argumenty / výchozí hodnoty

V definici funkce lze pomocí operátoru = přiřadit "posledním" (odzadu, jinak by zápis nebyl jednoznačně interpretovatelný) argumentům výchozí hodnoty, které poté při volání není potřeba vypisovat.

Například:

h(x, y=2) = x + y
h (generic function with 2 methods)
h(1)
3
h(1, 1)
2
methods(h)
# 2 methods for generic function h from Main:
  • h(x, y) in Main at In[5]:1
  • h(x) in Main at In[5]:1

I v tomto zápisu lze případně anotovat typ proměnné y pomocí ::.

gg(x, y::Int64=4) = x * y
gg (generic function with 2 methods)
gg(1, 2.5)
MethodError: no method matching gg(::Int64, ::Float64)
The function `gg` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  gg(::Any, ::Int64)
   @ Main In[9]:1
  gg(::Any)
   @ Main In[9]:1


Stacktrace:
 [1] top-level scope
   @ In[10]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
ch(x, y=2, z=4) = x ^ (y ^ z)
ch (generic function with 3 methods)
methods(ch)
# 3 methods for generic function ch from Main:
  • ch(x, y, z) in Main at In[12]:1
  • ch(x, y) in Main at In[12]:1
  • ch(x) in Main at In[12]:1
ch(2, 2, 3)
256
ch(2, 1)
2
ch(2)
65536

Operátor ..., "varargs"

Metody mohou mít přirozeně více argumentů. Následující funkce má právě tři poziční argumenty:

g(x, y, z) = x + y + z
g (generic function with 1 method)

Argumenty jí můžeme předat buď explicitně jeden po jednom, nebo je předat v tuple a použít ... operátor.

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

g(t...)
6

Ale pozor, následující volání selže. g nemá definovánu metodu, která by si poradila s tuplem na vstupu.

g(t)
MethodError: no method matching g(::Tuple{Int64, Int64, Int64})
The function `g` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  g(::Any, ::Any, ::Any)
   @ Main In[17]:1


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

Operátor ... můžeme použít i v definici funkce samotné. Umožňuje nám pak vytvořit funkci mající měnící se počet argumentů (variable number of arguments, "varargs").

Následující funkce má jeden nebo více pozičních argumentů:

H(x, args...) = println(args)
H (generic function with 1 method)
H(1)
()
()
()
typeof(())
Tuple{}
(,) # <- blbost
ParseError:
# Error @ In[27]:1:2
(,) # <- blbost
#└ ── Expected `)` or `,`

Stacktrace:
 [1] top-level scope
   @ In[27]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
(1)
1
typeof((1))
Int64
(1,)
(1,)
typeof((1,))
Tuple{Int64}
H(1, 2)
(2,)
H(1, 2, 3)
(2, 3)

Zamyslete se nad následujícími třemi ukázkami.

H(1, ("a", "b", "c"))
(("a", "b", "c"),)
H(1, ("a", "b", "c")...)
("a", "b", "c")
H(1, "a", "b", "c")

Typický reprezentant:

println(1, 2, 3, "Ahoj!")
123Ahoj!
vcat([1, 2], [3, 4], [4, 5])
6-element Vector{Int64}:
 1
 2
 3
 4
 4
 5
hcat([1, 2], [3, 4], [4, 5])
2×3 Matrix{Int64}:
 1  3  4
 2  4  5
CH(args::Int64...) = 1
CH (generic function with 1 method)
CH(1, "a")
MethodError: no method matching CH(::Int64, ::String)
The function `CH` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  CH(::Int64...)
   @ Main In[39]:1


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

Argumenty tvaru keyword=value (keyword arguments, "kwargs")

Jakmile počet argumentů přeroste jistou hranici (pro mě někde kolem 3), tak může být vhodné místo pozičního předávání argumentů použít zadávání pomocí klíče a hodnoty u kterých poté nezáleží na pořadí. Těmto argumentům také lze přiřazovat výchozí hodnoty. Takovéto argumenty od těch pozičních oddělíme středníkem ; v signatuře funkce:

G(x, y; operator=+) = operator(x, y)
G (generic function with 1 method)
G(1, 2)
3
G(2, 3, operator=*)
6

Lze i se středníkem za pozičními argumenty:

G(2, 3; operator=/)
0.6666666666666666
G(2, 3, *)
MethodError: no method matching G(::Int64, ::Int64, ::typeof(*))
The function `G` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  G(::Any, ::Any; operator)
   @ Main In[41]:1


Stacktrace:
 [1] top-level scope
   @ In[46]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
U(x; y) = x + y
U (generic function with 1 method)
U(1)
UndefKeywordError: keyword argument `y` not assigned

Stacktrace:
 [1] U(x::Int64)
   @ Main ./In[47]:1
 [2] top-level scope
   @ In[48]:1
 [3] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
U(1, y=42)
43
U(1, 2)
MethodError: no method matching U(::Int64, ::Int64)
The function `U` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  U(::Any; y)
   @ Main In[47]:1


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

Klíči nemusí být přiřazena výchozí hodnota, ale tuto možnost asi často nepoužijeme.

Julia podporuje i neomezený počet těchto argumentů. Uvažme lehce esoterickou funkci s následující signaturou:

function J(args...; kwargs...)
    println(typeof(args))
    println(typeof(kwargs))
    println(length(args))
    println(args)
    println(kwargs)
end
J (generic function with 1 method)
methods(J)
# 1 method for generic function J from Main:
  • J(args...; kwargs...) in Main at In[52]:1
J(1, 2, a=1, b="x")
Tuple{Int64, Int64}
Base.Pairs{Symbol, Any, Nothing, @NamedTuple{a::Int64, b::String}}
2
(1, 2)
Base.Pairs{Symbol, Any, Nothing, @NamedTuple{a::Int64, b::String}}(:a => 1, :b => "x")

V proměnné kwargs je poté "slovník", kde je pod symbolem odpovídajícím klíči uložena předávaná hodnota.

function kwargs_example(; kwargs...)
    println(keys(kwargs))
    println(kwargs[:a])
end
kwargs_example (generic function with 1 method)
kwargs_example(a=1)
(:a,)
1
kwargs_example(b=10)
(:b,)
FieldError: type NamedTuple has no field `a`, available fields: `b`

Stacktrace:
 [1] getindex
   @ ./namedtuple.jl:166 [inlined]
 [2] getindex
   @ ./iterators.jl:319 [inlined]
 [3] kwargs_example(; kwargs::@Kwargs{b::Int64})
   @ Main ./In[55]:3
 [4] kwcall(::@NamedTuple{b::Int64}, ::typeof(kwargs_example))
   @ Main ./In[55]:1
 [5] top-level scope
   @ In[57]:1
 [6] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
function křoví(x)
    return x + 1
end
křoví (generic function with 1 method)
křoví(1)
2

Poziční argumenty nelze předávat pomocí jejich názvu (klíče).

křoví(x=1)
MethodError: no method matching křoví(; x::Int64)
The function `křoví` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  křoví(::Any) got unsupported keyword argument "x"
   @ Main In[58]:1


Stacktrace:
 [1] top-level scope
   @ In[60]:1
 [2] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
roští(; x) = 1
roští (generic function with 1 method)
roští(x=1)
1

1.5 Návratová hodnota

V dosavadních příkladech metod jsme vždy vraceli hodnotu naposledy vyhodnoceného výrazu, často byl dokonce jenom jeden.

V mnoha situacích ale chceme vrátit hodnotu i z jiného místa, než konce těla. K tomu nepřekvapivě slouží klíčové slovo return.

function func(x::Integer)
    # some funny stuff
    for j = 1:10
        j >= x && return j
    end
    
    return 42
end
    
func(-10)
func(3)
func(15)

Pomocí tuplů můžeme vracet i "více" hodnot.

function func_tuple(x)
    return (x, x^2)
end
func_tuple (generic function with 1 method)

Pak je vhodné výsledek rovnou přiřadit do dvou proměnných:

a, b = func_tuple(10)
(10, 100)
a
10
b
100

Nebo můžeme samozřejmě přijmout celý tuple.

c = func_tuple(10)

c
(10, 100)

1.6 Předávání funkcí a do bloky

Funkce jsou v Julii hodnoty jako každé jiné, takže je můžeme předávat jako argumenty (funkce vyššího řádu). Zápis se často zkracuje pomocí do-bloku: funkce předaná jako první argument se zapíše až za voláním.

map(x -> x^2, [1, 2, 3])
3-element Vector{Int64}:
 1
 4
 9
map([1, 2, 3]) do x
    x^2
end

do-blok se hodí především tam, kde je funkce delší nebo kde chceme zdůraznit, co se s daty děje. Formálně jde o totéž jako předat anonymní funkci prvním argumentem:

function apply_twice(f, x)
    return f(f(x))
end

apply_twice(x -> x + 1, 3)

apply_twice(3) do x
    x + 1
end
5
apply_twice(3)
MethodError: no method matching apply_twice(::Int64)
The function `apply_twice` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  apply_twice(::Any, ::Any)
   @ Main In[77]:1


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

1.7 Volatelné objekty (funktory)

Metody nemusí patřit jen funkcím s vlastním jménem. Definujeme-li pro typ metodu ve tvaru (::NazevTypu)(args...), chová se jeho instance jako funkce – tzv. funktor. Připomeňme, že i konstruktory jsou vlastně jen metody.

struct Scale
    factor::Float64
end

(s::Scale)(x) = s.factor * x

double = Scale(2.0)
double(10)
20.0
map(Scale(3.0), [1, 2, 3])
3-element Vector{Float64}:
 3.0
 6.0
 9.0

2. Makra a metaprogramování

Pod metaprogramováním máme na mysli schopnost programu generovat, či modifikovat, svůj zdrojový kód. Makra v Julii jsou inspirována Lispem. Nejde jen o pouhé textové transformace jako v případě maker v C/C++. V Julii má programátor přímý přístup k vnitřní reprezentaci zdrojového kódu.


2.1 Výrazy a AST

Ukažme si, jak Julia zpracovává zdrojový kód. Na počátku máme řetězec. Například:

source = "1 + 2"
"1 + 2"

Co s tímto zdrojovým kódem udělá parser jazyka Julia? Vytvoří objekt typu Expr:

ex = Meta.parse(source)
:(1 + 2)
typeof(ex)
Expr

Objekt typu Expr obsahuje v zásadě dvě informace:

ex.head # symbol udávající význam
:call
typeof(ex.head)
Symbol
ex.args # argumenty
3-element Vector{Any}:
  :+
 1
 2

Přehledně lze tyto informace vypsat pomocí metody dump:

dump(ex)
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Int64 1
    3: Int64 2

Interpretace tohoto příkladu je nasnadě. Reprezentuje volání (call) metody + s Int64 argumenty 1 a 2.

Tyto výrazy lze přirozeně vytvářet i přímo pomocí konstruktoru Expr. Za chvilku si ale ukážeme další způsoby, jak výrazy vytvářet, tento by nebyl příliš efektivní.

myex = Expr(:call, :+, 1, 2)
:(1 + 2)
ex == myex
true

Výrazy dohromady vytváří stromovou strukturu (AST -- abstract syntax tree), lze je do sebe zanořovat:

dump(Meta.parse("(1 + 2) / 3"))

A konečně, výrazy můžeme finálně vyhodnotit pomocí metody eval.

dump(ex)
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Int64 1
    3: Int64 2
dump(myex)
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Int64 1
    3: Int64 2
eval(ex)
3
eval(myex)
3
ex2 = Expr(:call, :neznámá_věc, 1, 2)
:(neznámá_věc(1, 2))
dump(ex2)
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol neznámá_věc
    2: Int64 1
    3: Int64 2
eval(ex2)
UndefVarError: `neznámá_věc` not defined in `Main`
Suggestion: check for spelling errors or missing imports.

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

2.2 Quote a interpolace

Dalším způsobem vytváření objektů typu Expr je quoting (kód v uvozovkách :-)). Toho lze docílit dvěma způsoby. Pro menší výrazy se hodí zápis pomocí :, za kterou v závorce uvedeme výraz v Julii:

myex2 = :((1 + 2) / 3)
:((1 + 2) / 3)
typeof(myex2)
Expr
dump(:(f(1 + 1)))
Expr
  head: Symbol call
  args: Array{Any}((2,))
    1: Symbol f
    2: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol +
        2: Int64 1
        3: Int64 1

Proměnné se ve výrazu uloží pod symboly, ne pod svými hodnotami!

x = 1
dump(:(x + 1))
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Symbol x
    3: Int64 1

Pokud bychom chtěli použít skutečně hodnotu proměnné, pak k tomu můžeme použít interpolační symbol $:

x = 1
dump(:($x + 1))
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Int64 1
    3: Int64 1

Pro větší výrazy můžeme použít quote blok.

ex = quote
    for j = 1:10
        println(j)
    end
end
quote
    #= In[102]:2 =#
    for j = 1:10
        #= In[102]:3 =#
        println(j)
        #= In[102]:4 =#
    end
end
typeof(ex)
Expr
dump(ex)
Expr
  head: Symbol block
  args: Array{Any}((2,))
    1: LineNumberNode
      line: Int64 2
      file: Symbol In[102]
    2: Expr
      head: Symbol for
      args: Array{Any}((2,))
        1: Expr
          head: Symbol =
          args: Array{Any}((2,))
            1: Symbol j
            2: Expr
              head: Symbol call
              args: Array{Any}((3,))
                1: Symbol :
                2: Int64 1
                3: Int64 10
        2: Expr
          head: Symbol block
          args: Array{Any}((3,))
            1: LineNumberNode
              line: Int64 3
              file: Symbol In[102]
            2: Expr
              head: Symbol call
              args: Array{Any}((2,))
                1: Symbol println
                2: Symbol j
            3: LineNumberNode
              line: Int64 4
              file: Symbol In[102]
eval(ex)
1
2
3
4
5
6
7
8
9
10

2.3 Generování kódu: @eval a @generated

Výrazy umíme sestavit a vyhodnotit. To využijeme, když chceme programově generovat metody – nejčastěji pro celou řadu jmen najednou. K tomu slouží makro @eval, což je pohodlnější zápis za eval s citovaným výrazem (a funguje v něm interpolace přes $).

struct Point
    x::Float64
    y::Float64
end

for f in (:sin, :cos, :exp)
    @eval Base.$f(p::Point) = Point($f(p.x), $f(p.y))
end

sin(Point(1.0, 0.0))
Point(0.8414709848078965, 0.0)
cos(Point(2.0, -1.0))
Point(-0.4161468365471424, 0.5403023058681398)

Silnější nástroj je generovaná funkce (@generated). Její tělo Julia sestaví až při první specializaci metody na konkrétní typy argumentů a udělá to jen jednou pro každou kombinaci typů. Tělo tedy smí pracovat jen s informacemi dostupnými v době kompilace – typicky s typy argumentů, nikoli s jejich hodnotami.

@generated function mysum(x::Tuple)
    N = fieldcount(x)
    ex = :(x[1])
    for i = 2:N
        ex = :($ex + x[$i])
    end
    return ex
end

mysum((1, 2.5, 3))

Ačkoli je @generated mocné, obecně platí: dokud to jde, pište obyčejné metody. Generované funkce se hodí hlavně pro výkonnostní optimalizace a musí se psát obezřetně (např. nesmí volat funkce s vedlejšími efekty).


2.4 Makra

Makra přijímají jako argumenty výrazy (Expr), literály nebo symboly a vracejí výraz (Expr). Makra definujeme pomocí klíčového slova macro. Ve zdrojovém kódu poté makra používáme s prefixem @. Makra se aplikují při parsování zdrojového kódu, před jeho odesláním kompilátoru.

Ukažme si nejprve, jak je v AST reprezentováno volání funkce.

f(a, b) = a + b
dump(Meta.parse("f(x, y)"))

Následující makro vypíše argumenty volání funkce.

macro show_args(expr::Expr)
    println("Arguments: ", expr.args[2:end])
end

Například:

@show_args f(1, 2)
x = 42; y = 11

@show_args f(x, y)
dump(:(f(x, y)))
dump(:(f(1, 2.5)))

To není přesně to, co bychom asi chtěli (i když...). Pokud chceme vypsat i hodnoty proměnných, musíme trochu zapracovat....

macro show_args(expr::Expr)
    println("Arguments: ")
    
    for arg in expr.args[2:end]
        if typeof(arg) == Symbol
            println(arg, " = ", eval(:($arg)))
        else
            println(arg)
        end
    end
end
@show_args f(1, 2)
@show_args f(x, y)
@show_args f(1, x)
@show_args f(x, z)

Občas pomocí maker chceme modifikovat prostředí, ve kterém se volají. K tomu můžeme použít metodu esc. V prvním případě je x pouze lokální proměnná a nemá vztah ke "globální" proměnné x.

macro zerox()
    return :(x = 0)
end

macro zerox2()
    return esc(:(x = 0))
end
x = 42
@zerox
x
@zerox2
x

Psaní maker nemusí být úplně jednoduché. Více o této problematice se můžete dozvědět v dokumentaci.

Ve zbytku této části si ukážeme některá užitečná makra a zkusíme pár vlastních maker vytvořit. Výčet není zdaleka vyčerpávající a jde spíše o ochutnávku. Některým z těchto partií se ještě budeme věnovat později během semestru.


2.5 Kontrola expanze maker: @macroexpand a @macroexpand1

Při psaní maker je užitečné vidět, co z nich vlastně vznikne. Makro @macroexpand vrátí výsledný výraz po rekurzivní expanzi všech maker, @macroexpand1 expanduje jen jednu (nejvnější) úroveň.

macro twice(ex)
    return :($(esc(ex)); $(esc(ex)))
end

macro squared(ex)
    return :($(esc(ex))^2)
end
@macroexpand1 @twice @squared(1 + 1)
@macroexpand @twice @squared(1 + 1)

Na prvním výstupu zůstalo @squared ještě neexpandované, druhý výstup už obsahuje plně expandovaný kód.


2.6 @which

Jak již bylo zmíněno, pod jedním symbolem "funkce" se může skrývat mnoho a mnoho metod. Občas nemusí být úplně jasné, která z nich se vlastně volá. Makro @which nám umožňuje dohledat o kterou metodu se v konkrétním případě jedná.

@which 2^3
literal_pow(::typeof(^), x::Union{Float16, Float32, Float64, Int16, Int32, Int64, Int8, UInt16, UInt32, UInt64, UInt8, Complex{<:Union{Float16, Float32, Float64, Int16, Int32, Int64, Int8, UInt16, UInt32, UInt64, UInt8}}, Rational{<:Union{Float16, Float32, Float64, Int16, Int32, Int64, Int8, UInt16, UInt32, UInt64, UInt8}}}, ::Val{3}) in Base at intfuncs.jl:468
@which 2.0^3.0
^(x::Float64, y::Float64) in Base.Math at special/pow.jl:7
g1(x::Int64) = x + 1
g1(x::Float64) = x - 1
g1 (generic function with 2 methods)
@which g1(1.0)
g1(x::Float64) in Main at In[122]:2
@which g1(1)

Podobně se chová makro @edit, které rovnou otevře zdrojový kód.

@edit 3^2
Vim: Warning: Output is not to a terminal
Vim: Warning: Input is not from a terminal
[?25l[?25h
Error detected while processing /home/kalvin/.vimrc:
line   31:
E492: Not an editor command: Plug 'JuliaEditorSupport/julia-vim'
Press ENTER or type command to continue
InterruptException:

Stacktrace:
  [1] try_yieldto(undo::typeof(Base.ensure_rescheduled))
    @ Base ./task.jl:1172
  [2] wait()
    @ Base ./task.jl:1299
  [3] wait(c::Base.GenericCondition{Base.Threads.SpinLock}; first::Bool)
    @ Base ./condition.jl:141
  [4] wait
    @ ./condition.jl:136 [inlined]
  [5] wait(x::Base.Process, syncd::Bool)
    @ Base ./process.jl:706
  [6] wait
    @ ./process.jl:699 [inlined]
  [7] success
    @ ./process.jl:568 [inlined]
  [8] run(::Cmd; wait::Bool)
    @ Base ./process.jl:525
  [9] run
    @ ./process.jl:522 [inlined]
 [10] (::InteractiveUtils.var"#3#4"{…})(cmd::Cmd, path::String, line::Int32, column::Int64)
    @ InteractiveUtils /usr/share/julia/stdlib/v1.13/InteractiveUtils/src/editless.jl:103
 [11] edit(path::String, line::Int32, column::Int64)
    @ InteractiveUtils /usr/share/julia/stdlib/v1.13/InteractiveUtils/src/editless.jl:242
 [12] edit(path::String, line::Int32)
    @ InteractiveUtils /usr/share/julia/stdlib/v1.13/InteractiveUtils/src/editless.jl:232
 [13] edit(argtypes::Union{Tuple, Type{<:Tuple}})
    @ InteractiveUtils /usr/share/julia/stdlib/v1.13/InteractiveUtils/src/editless.jl:273
 [14] top-level scope
    @ In[125]:1
 [15] macro expansion
    @ /usr/share/julia/stdlib/v1.13/InteractiveUtils/src/macros.jl:254 [inlined]
 [16] eval(m::Module, e::Any)
    @ Core ./boot.jl:489
Some type information was truncated. Use `show(err)` to see complete types.

2.7 @debug, @info, @warn, @error

Tato makra slouží k informování uživatele, lze i kontrolovat, na jaké úrovni se logování provádí (k tomu slouží modul Logging, kterému se budeme věnovat při probírání standardní knihovny).

@debug "???"
@info "Hi!"
[ Info: Hi!
@warn "Beware!"
┌ Warning: Beware!
└ @ Main In[128]:1
@error "Unable to compute!"
┌ Error: Unable to compute!
└ @ Main In[129]:1

2.8 Rychlá kontrola a ladění: @assert a @show

@assert ověří podmínku a při jejím nesplnění vyvolá AssertionError. @show vypíše výraz i s jeho hodnotou – hodí se při ladění i při výkladu.

@assert 1 + 1 == 2
@show 2 + 2
x = 3
@show x x^2
# @assert 1 + 1 == 3  # vyvolalo by AssertionError

2.9 @time, @timed a @timev

Pomocí těchto maker můžeme měřit dobu běhu programu. Jde o jednodušší variantu makra @benchmark z balíčku BenchmarkTools.jl. Tato tři makra se liší pouze způsobem výstupu.

a = rand(1_000);
a[1:10]
10-element Vector{Float64}:
 0.7437881767476621
 0.725196517218191
 0.4541780539427138
 0.029581780796472223
 0.9475250352695385
 0.3681260907190166
 0.27903650723244056
 0.31227638395478596
 0.0016994093584039582
 0.33270251080558855
@time sort(a)
  0.000025 seconds (9 allocations: 17.898 KiB)
1000-element Vector{Float64}:
 0.0006515149116667063
 0.0016994093584039582
 0.002029974940028656
 0.002247879193459812
 0.0037765344107584253
 0.0038824643192691966
 0.005646922395127096
 0.006120174879656881
 0.007033987051101476
 0.007545760114249456
 0.007900083748029862
 0.008153248119636247
 0.00841516861508318
 ⋮
 0.9936788115472469
 0.9941333206367915
 0.9951558682897199
 0.9959542799075708
 0.9974727119077195
 0.9975569694336669
 0.9978273691571466
 0.9982848330126755
 0.9988343216147826
 0.9991325835617975
 0.9993081083152998
 0.9997820320607193

d jako dictionary:

@timed sort(a)
(value = [0.0006515149116667063, 0.0016994093584039582, 0.002029974940028656, 0.002247879193459812, 0.0037765344107584253, 0.0038824643192691966, 0.005646922395127096, 0.006120174879656881, 0.007033987051101476, 0.007545760114249456  …  0.9951558682897199, 0.9959542799075708, 0.9974727119077195, 0.9975569694336669, 0.9978273691571466, 0.9982848330126755, 0.9988343216147826, 0.9991325835617975, 0.9993081083152998, 0.9997820320607193], time = 2.4106e-5, bytes = 18328, gctime = 0.0, gcstats = Base.GC_Diff(18328, 3, 0, 6, 0, 0, 0, 0, 0), lock_conflicts = 0, compile_time = 0.0, recompile_time = 0.0)

v jako verbose, čili podrobnější:

@timev sort(a)
  0.000023 seconds (9 allocations: 17.898 KiB)
elapsed time (ns):  22795.0
gc time (ns):       0
bytes allocated:    18328
pool allocs:        6
non-pool GC allocs: 0
malloc() calls:     3
free() calls:       0
minor collections:  0
full collections:   0
1000-element Vector{Float64}:
 0.0006515149116667063
 0.0016994093584039582
 0.002029974940028656
 0.002247879193459812
 0.0037765344107584253
 0.0038824643192691966
 0.005646922395127096
 0.006120174879656881
 0.007033987051101476
 0.007545760114249456
 0.007900083748029862
 0.008153248119636247
 0.00841516861508318
 ⋮
 0.9936788115472469
 0.9941333206367915
 0.9951558682897199
 0.9959542799075708
 0.9974727119077195
 0.9975569694336669
 0.9978273691571466
 0.9982848330126755
 0.9988343216147826
 0.9991325835617975
 0.9993081083152998
 0.9997820320607193

2.10 @inbounds a @simd

Makro @inbounds "vypne" kontrolu mezí (bounds checking) při indexování polí. Makro @simd umožňuje kompilátoru větší možnosti optimalizace for cyklu, viz dokumentaci.

a = [1, 2, 3, 4]
4-element Vector{Int64}:
 1
 2
 3
 4
a[2]
2
a[5]
BoundsError: attempt to access 4-element Vector{Int64} at index [5]

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[138]:1
 [5] eval(m::Module, e::Any)
   @ Core ./boot.jl:489
using BenchmarkTools
function f1(a::Vector{Float64}, n)
    val = 0.0
    for j = 1:n
        val += a[j]
    end
    return val
end

function f2(a::Vector{Float64}, n)
    val = 0.0
    @simd for j = 1:n
        @inbounds val += a[j]
    end
    return val
end

function f3(a::Vector{Float64}, n)
    val = 0.0
    for j = 1:n
        @inbounds val += a[j]
    end
    return val
end

function f4(a::Vector{Float64}, n)
    val = 0.0
    @simd for j = 1:n
        val += a[j]
    end
    return val
end
f4 (generic function with 1 method)
a = rand(10^8);
f1(a, 10^6)
499986.9829983195
f2(a, 10^6)
499986.9829983291
f3(a, 10^6)
499986.9829983195
f4(a, 10^6)
499986.9829983291
@benchmark f1($a, 10^6)
BenchmarkTools.Trial: 3474 samples with 1 evaluation per sample.
 Range (min … max):  1.206 ms …   3.382 ms  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.250 ms               ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.438 ms ± 339.215 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%

  █▄▃▂▂▂▁▁▁▁▁▁▁  ▁▁   ▁    ▁                                   
  █████████████████████████████▇█▇███▇█▇▇▆▆▇▅▆▆▄▄▅▃▄▂▂▄▅▃▃▄▆▅ █
  1.21 ms      Histogram: log(frequency) by time      2.63 ms <

 Memory estimate: 0 bytes, allocs estimate: 0.
@benchmark f2($a, 10^6)
BenchmarkTools.Trial: 10000 samples with 1 evaluation per sample.
 Range (min … max):  244.809 μs …   2.174 ms  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     315.895 μs               ┊ GC (median):    0.00%
 Time  (mean ± σ):   426.422 μs ± 243.198 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%

  ▂▆▇█▆▅▄▃▃▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▁             ▁▁▁                     ▂
  ████████████████████████████▇▇▇▆▇▇▇▇▇█████████▇▇▇█▆▇▅▇▆▇▆▆▇▆▆ █
  245 μs        Histogram: log(frequency) by time       1.33 ms <

 Memory estimate: 0 bytes, allocs estimate: 0.
@benchmark f3($a, 10^6)
BenchmarkTools.Trial: 3458 samples with 1 evaluation per sample.
 Range (min … max):  1.205 ms …   3.351 ms  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     1.255 ms               ┊ GC (median):    0.00%
 Time  (mean ± σ):   1.444 ms ± 344.118 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%

  █▄▃▃▂▂▁▁▁▁▁▁▁▁     ▁▁▁ ▁▁                                    
  ███████████████████████████████▇███▇█▇▆▆▆▆▅▄▄▅▄▄▄▅▂▄▄▅▅▆▅▄▄ █
  1.21 ms      Histogram: log(frequency) by time      2.69 ms <

 Memory estimate: 0 bytes, allocs estimate: 0.
@benchmark f4($a, 10^6)
BenchmarkTools.Trial: 9735 samples with 1 evaluation per sample.
 Range (min … max):  257.421 μs …   3.960 ms  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     358.121 μs               ┊ GC (median):    0.00%
 Time  (mean ± σ):   512.040 μs ± 325.371 μs  ┊ GC (mean ± σ):  0.00% ± 0.00%

  ▄▇██▆▅▄▃▃▃▃▂▃▃▃▃▂▂▂▂▁▁▁▁▁▁▁▂▂▂▁▂▁▁▁ ▁    ▁                    ▂
  ███████████████████████████████████████████▇██▇▇▇▆▇▇▅▆▆▅▅▃▆▅▅ █
  257 μs        Histogram: log(frequency) by time       1.75 ms <

 Memory estimate: 0 bytes, allocs estimate: 0.

2.11 @test a @testset

Tato makra z modulu Test nám umožňují přehledně testovat náš kód. Pomocí @testset můžeme sdružit více testů dohromady a pojmenovat je (bere řetězec a blok). Druhé makro testuje, zda je výraz pravdivý, nebo nepravdivý. Podrobněji se testování a modulu Test budeme věnovat v lekci 06. Například:

using Test

@testset "isodd method" begin
    @test isodd(3) == true
    @test isodd(2) == false
end

Dále máme k dispozici @test_throws pro testování vyvolání výjimky.


2.12 @code_native a další introspekce

a.k.a "We need to go deeper..."

Rodina příbuzných maker nám umožňuje nahlédnout do jednotlivých fází zpracování kódu: @code_lowered (snížený IR), @code_typed (typovaný IR), @code_llvm (LLVM IR), @code_native (strojový kód) a @code_warntype (upozornění na typovou nestabilitu).

function g(x::Int64)
    return x + 1
end
g (generic function with 2 methods)
@code_native g(2)
	.file	"g"
	.section	.ltext,"axl",@progbits
	.globl	julia_g_8329                    # -- Begin function julia_g_8329
	.p2align	4
	.type	julia_g_8329,@function
julia_g_8329:                           # @julia_g_8329
; Function Signature: g(Int64)
; ┌ @ In[150]:1 within `g`
# %bb.0:                                # %top
	#DEBUG_VALUE: g:x <- $rdi
	push	rbp
	mov	rbp, rsp
; │ @ In[150]:2 within `g`
; │┌ @ int.jl:87 within `+`
	lea	rax, [rdi + 1]
; │└
	pop	rbp
	ret
.Lfunc_end0:
	.size	julia_g_8329, .Lfunc_end0-julia_g_8329
; └
                                        # -- End function
	.section	".note.GNU-stack","",@progbits
@code_lowered g(2)
CodeInfo(
1 ─ %1 = Main.:+
│   %2 =   dynamic (%1)(x, 1)
└──      return %2
)
@code_typed g(2)
CodeInfo(
1 ─ %1 = intrinsic Base.add_int(x, 1)::Int64
└──      return %1
) => Int64
@code_warntype g(2)
MethodInstance for g(::Int64)
  from g(x::Int64) @ Main In[150]:1
Arguments
  #self#::Core.Const(Main.g)
  x::Int64
Body::Int64
1 ─ %1 = Main.:+::Core.Const(+)
│   %2 = (%1)(x, 1)::Int64
└──      return %2

using ProgressMeter
@showprogress for i in 1:50
    sleep(0.1)
end
@showprogress dt=1 desc="Computing..." for i in 1:50
    sleep(0.1)
end

2.14 Struktury s klíčovými argumenty: @kwdef

Ruční psaní konstruktoru se všemi klíčovými argumenty a jejich výchozími hodnotami je zdlouhavé. Makro @kwdef takový konstruktor vygeneruje za nás.

@kwdef struct Config
    steps::Int    = 100
    tol::Float64  = 1e-6
    verbose::Bool = false
end

Config()
Config(steps=10, verbose=true)

Cvičení

Vytvořte makro @dotimes, které zadaný výraz provede několikrát za sebou. Přesněji @dotimes n body provede body přesně nkrát.

macro dotimes(n, body)
  quote
    for i = 1:$n
      $body
    end
  end
end
@dotimes 2 println("Hi!")
body = 1
n = 3

@dotimes 2 println("Hi!")
body
n

Cvičení: generování kódu

Definujme vlastní "číselný" typ:

struct MyNumber <: Number
    value::Float64
end

Vygenerujte kód, který zadefinuje funkce sin, cos, log, exp pro tento typ.

for func in [:sin, :cos, :log, :exp]
    eval(:(Base.$func(x::MyNumber) = MyNumber($func(x.value))))
end
sin(MyNumber(0.3))
cos(MyNumber(0.3))
exp(MyNumber(0.3))
log(MyNumber(0.3))

Cvičení: Sledování průběhu for cyklu

Vytvořte makro, které bude zobrazovat jednoduchý průběh vyhodnocování for cyklu. Tj.

@progress for j = 1:n
    # ...
end

bude efektivně

for j = 1:n
    # ...
    println(j)
end

Případně se můžete pokusit výpis i více zkrášlit.

Zde jde samozřejmě o cvičení práce s makry. Julia jinak má poměrně excelentní balíček ProgressMeter.jl poskytující přesně tuto funkcionalitu.

macro progress(expr)
    push!(expr.args[2].args, :(println(j)))
    return expr
end
@progress (macro with 1 method)
@progress for j=1:5
    sleep(1)
end
1
2
3
4
5

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

Inverze.

import Base.inv

inv(p::MyRational{T}) where { T <: Integer } = MyRational(p.den, p.num)
inv(p)
p^(-3)

Komplexní čísla.

import Base.*, Base.+, Base.-, Base.inv, Base./

*(u::MyComplex{S}, v::MyComplex{T}) where {S, T} =
    MyComplex(u.re * v.re - u.im * v.im, u.re * v.im + u.im * v.re)

+(u::MyComplex{S}, v::MyComplex{T}) where {S, T} =
    MyComplex(u.re + v.re, u.im + v.im)

-(u::MyComplex{T}) where {T} = MyComplex(-u.re, -u.im)

-(u::MyComplex{S}, v::MyComplex{T}) where {S, T} = u + (-v)

function inv(u::MyComplex{T}) where {T}
    d = u.re^2 + u.im^2
    return MyComplex(u.re / d, -u.im / d)
end

/(u::MyComplex{S}, v::MyComplex{T}) where {S, T} = u * inv(v)
z = MyComplex(1, -2)
w = MyComplex(3, 4)

z + w, z * w, z / w, inv(z)

Modulární multiplikativní grupa.

import Base.*, Base.inv, Base.show
using Primes

struct MMG{T <: Integer, P} <: Number
    value::T
    
    function MMG(value::T, modulus::T) where { T <: Integer }
        isprime(modulus) || error("Modulus has to be prime!")
        my_value = mod(value, modulus)
        iszero(my_value) && error("0 is not acceptable!")
        
        new{T, modulus}(my_value)
    end
end

modulus(u::MMG{T, P}) where { T <: Integer, P } = P

function *(a::MMG{T, P}, b::MMG{T, P}) where { T <: Integer, P }
    return MMG(mod(a.value * b.value, P), P)
end

function inv(a::MMG{T, P}) where { T <: Integer, P }
    # d = u * a + v * P
    d, u, v = gcdx(a.value, P)
    
    return MMG(mod(u, P), P)
end

show(io::IO, u::MMG) = print(io, "|", u.value, "|_", modulus(u))

Makro dotimes.

macro dotimes(n, body)
  quote
    for i = 1:$n
      $body
    end
  end
end

Průběh for cyklu.

macro progress(expr)
    expr.head === :for || error("@progress očekává `for` cyklus")

    var  = expr.args[1].args[1]   # jméno řídící proměnné, např. :j
    body = expr.args[2]
    if !(body isa Expr && body.head === :block)
        body = Expr(:block, body)
        expr.args[2] = body
    end
    push!(body.args, :(println($var)))

    return esc(expr)
end

Reference

V oficiální dokumentaci této problematice odpovídají sekce Methods a Macros.