Jdi na navigaci předmětu

08: Moduly, balíčky, dokumentace

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 2023/2024 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.9.3
Commit bed2cd540a1 (2023-08-24 14:43 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
  LIBM: libopenlibm
  LLVM: libLLVM-14.0.6 (ORCJIT, skylake)
  Threads: 2 on 8 virtual cores

1. Moduly

K sdružení souvisejících funkcí, metod a typů Julia nabízí moduly. Moduly poskytují oddělený jmenný prostor (namespace) a lze se tak vyhnout problémům s pojmenováním metod, proměnných, atd.

Struktura modulu je následující


module ModuleName

# importy, exporty...

# kód

end

Tělo modulu bývá zvykem neodrážet od kraje. Importy použitých funkcí a dalších modulů bývá zvykem uvádět hned na začátku.

Tělo modulu také lze rozdělit do více souborů a použít pak metodu include. Tato metoda načte kód ze zadaného souboru. Z pohledu Julia se pak tento kód chápe jako kdyby byl na místě, kde bylo volání include.

Jako příklad uvažme následující modul.

"""
Demonstrační modul.
"""
module MyModule

import Base.show
export Dimensional, my_circle_area

"""
Zvrhlá konstanta.
"""
const MyPi = 3.14159#ish

"""
Náš užasný typ "rozměrného" čísla s jednotkou.
"""
struct Dimensional{T <: Number}
    value::T
    unit::String
end

show(io::IO, x::Dimensional{T}) where { T <: Number } =
    print(io, "$(x.value)$(x.unit)")

"""
Obsah kruhu o daném poloměru.
"""
function my_circle_area(radius::Dimensional{T}) where { T <: Number }
    return Dimensional(MyPi * radius.value^2, radius.unit * "²")
end

"""
Obsah kvádru o zadaných délkách stran.
"""
function my_cuboid_volume(a::Dimensional{T}, b::Dimensional{T}, c::Dimensional{T}) where { T <: Number }
    return Dimensional(a.value * b.value * c.value, a.unit * "×" * b.unit * "×" * c.unit)
end

end # module
Main.MyModule

Všimněte si, že předchozí buňka po vyhodnocení vrátila Main.MyModule. V notebooku, nebo v REPL, pracujeme v Main modulu. Předchozí buňkou jsme tedy fakticky definovali podmodul modulu Main.

MyModule == Main.MyModule
true

Následující ukázky očekávají, že jsme tento modul ještě neimportovali, případně pro jistotu restartujte kernel.

V jmenném prostoru notebooku modul máme k dispozici

MyModule
Main.MyModule
# MyModule.

K jednotlivým metodám, typům a konstantám definovaných v modulu přístup nemáme (myšleno v našem namespace).

MyPi
UndefVarError: `MyPi` not defined
Dimensional
UndefVarError: `Dimensional` not defined
my_circle_area
UndefVarError: `my_circle_area` not defined

Cestu k metodám, typům nebo konstantam musíme (zatím, pokud jsme žádné neexportovali) specifikovat pomocí .:

MyModule.MyPi
3.14159
Main.MyModule.MyPi
3.14159
r = MyModule.Dimensional(2, "m")
2m
MyModule.my_circle_area(r)
12.56636m²

V některých případech je potřeba za tečkou použít dvojtečku (a vytvoření symbolu), nebo závorky, abychom se vyhnuli syntaktickým problémů:

typeof(:+)
Symbol
typeof(+)
typeof(+) (singleton type of function +, subtype of Function)
Base.:+
+ (generic function with 207 methods)
Base.:(==)
== (generic function with 181 methods)

Každopádně, tato nutnost opisovat název modulu jistě není úplně požadované chování. Typicky bychom s některými metodami (nebo typy) chtěli pracovat přímo v našem prostředí a ne se na ně takto podrobně odvolávat pomocí modulu. Jak to udělat si ukážeme za pár okamžiků.

V modulu vytvořeném způsobem popsaným výše už jsou automaticky inportovány exportované metody z modulů Core a Base a metody include a eval. Pokud bychom chtěli vytvořit "prázdný" modul, musíme použít baremodule na místo module. Definice modulu je v podstatě následující:

baremodule Module

using Core, Base

eval(x) = Core.eval(Module, x)
include(p) = Base.include(Module, p)

#...

end

Dostupné metody lze prozkoumat

# MyModule.

1.1 Standardní moduly

V předchozí sekci jsme zmínili moduly Core, Base a Main. V Julia jsou to tři "speciální" moduly, jsou vždy k dispozici:

  • Core: funkcionalita obsažená přímo "v jazyce" (prozkoumejte nabídku níže),
  • Base: funkcionalita užitečná ve "většině situací",
  • Main: modul, v kterém se pracuje při spuštění Julia REPL (nebo i zde v notebooku).
# Core.
# Base.
# Main.

Aktuální modul lze získat pomocí esotericky pojmenovaného makra @__MODULE__.

@__MODULE__
Main

1.1 import

K importu požadované metody implementované v jistém modulu lze nepřekvapivě použít klíčové slovo import, a to hned několika způsoby:

import ModuleName                      # prostý import modulu

import ModuleName as OtherName         # import s přejmenováním

import ModuleName: stuff               # import dané metody/typu/...

import ModuleName: stuff as other_name # import a přejmenování dané metody/typu/...

Import zcela ignoruje seznam exportovaných objektů. Pojďme si tyto možnosti postupně rozebrat. Protože jsme modul MyModule definovali přímo zde v notebooku, musíme k němu udat plnou cestu, což je Main.Module.

Obyčejný import do jmenného prostoru zavede modul samotný (ten u nás zde v notebooku už máme) a jinak nic dalšího.

import Main.MyModule
MyPi
UndefVarError: `MyPi` not defined
Dimensional
UndefVarError: `Dimensional` not defined
my_circle_area
UndefVarError: `my_circle_area` not defined

V některých situacích může být vhodné importovaný modul přejmenovat, abychom se nedostali do konfliktu s pojmenováním. K tomu slouží as:

import Main.MyModule as MM
MM
Main.MyModule

Stále ale nemáme přímý přistup k metodám a typů v modulu. Dále můžeme jednotlivě vybrané objekty zavést i do aktuálního jmenného prostoru.

import Main.MyModule: MyPi, Dimensional
import Main.MyModule: my_circle_area as circle_area
MyPi
3.14159
Dimensional
Dimensional
circle_area
my_circle_area (generic function with 1 method)
my_cuboid_volume
UndefVarError: `my_cuboid_volume` not defined

Pozor, * nefunguje tak jak byste čekali (co se přesně stalo?).

import Main.MyModule: *
my_cuboid_volume
UndefVarError: `my_cuboid_volume` not defined
*
* (generic function with 309 methods)

1.2 using

V tento moment prosím restartujte jádro a znovu vyhodnoťte buňku s definicí našeho ukázkového modulu.

using se chová podobně jako import. Následující kód do jmenného prostoru zavede modul (u nás už ho máme) a dále všechny modulem exportované objekty.

using Main.MyModule
MyModule
Main.MyModule
MyPi
UndefVarError: `MyPi` not defined
Dimensional
Dimensional

Konečně,

using Module: stuff

je ekvivalentní

import Module: stuff

Také se načtou pouze zmíněné metody, ne všechny exportované.

Seznam všech exportovaných metod a typů lze získat pomocí metody names.

names(MyModule)
3-element Vector{Symbol}:
 :Dimensional
 :MyModule
 :my_circle_area

Například (restart jádra!):

using .MyModule: MyPi
MyPi
3.14159
Dimensional
UndefVarError: `Dimensional` not defined

Podmoduly

Moduly mohou přirozeně obsahovat podmoduly. Podmoduly jsou zcela odděleny od svých předků.

Předka modulu lze zjistit pomocí metody parentmodule.

parentmodule(MyModule)
Main
parentmodule(Main)
Main
parentmodule(Base)
Main
parentmodule(Core)
Core

2. Projekty

Julia projekty umožňují definovat jaké balíčky a v jakých verzích se při výpočtu (nebo jakékoliv další jiné aktivitě) použily. Zajišťují správu závislostí a tím podporují reprodukovatelnost výpočtu. Nemělo by se vám později stát, že najednou kód nefunguje, protože se používá balíček v jiné verzi.

Projekt v podstatě není nic jiného než adresář (lépe git repozitář) obsahující dva soubory

  • Project.toml: přímé závislosti, (případně název projektu a unikátní identifikátor),
  • Manifest.toml: všechny (i nepřímé) závislosti.

2.1 Vytvoření projektu

Projekt lze vytvořit velmi snadno. Spustíme Julia v adresáři, kde chceme vytvořit adresář s projektem, přepneme se do Pkg módu a zadáme následující příkaz

(@v1.9) pkg> generate MyProject
  Generating  project MyProject: 
    MyProject/Project.toml
    MyProject/src/MyProject.jl

Prostředí již existujícího projektu pak aktivujeme příkazem (případně ] activate ., jsme-li v adresáři projektu; nebo parametrem --project při startu Julia, viz níže):

(@v1.9) pkg> activate MyProject
  Activating new environment at `~/documents/fit/B231-BI-JUL/MyProject/Project.toml`

(MyProject) pkg>

Všimněte si změny v příkazové řádce. Julia nám naznačuje, že nepracujeme v globálním prostředí, ale v projektu MyProject. Pokud byste zkusili importovat balíčky, které jste dříve instalovali, tak tato operace selže. Toto prostředí je zatím "prázdné".


2.2 Přidávání balíčků do projektu

Přidejme si do něho třeba balíček pro práci s prvočísly Primes,

(MyProject) pkg> add Primes
    Updating registry at `~/.julia/registries/General`
    Updating git-repo `https://github.com/JuliaRegistries/General.git`
   Resolving package versions...
    Updating `~/documents/fit/B231-BI-JUL/MyProject/Project.toml`
  [18e54dd8] + IntegerMathUtils v0.1.2
  [27ebfcd6] + Primes v0.5.4
    Updating `~/documents/fit/B231-BI-JUL/MyProject/Manifest.toml`
  [27ebfcd6] + Primes v0.5.4

Všimněte si, že byl vytvořen další soubor, Manifest.toml. Pokud nyní opustíte Julia a přesunete se do adresáře MyProject, pak zde najdete dva TOML soubory.


2.3 Obsah Project.toml:

[deps]
Primes = "27ebfcd6-29c5-5fa9-bf4b-fb8fc14df3ae"

Tento soubor ale může obsahovat i další užitečné informace, viz dokumentaci Pkg.jl.

Například lze definovat minimální podporovanou Julia verzi.

[compat]
julia = "1.6"

2.4 Obsah Manifest.toml:

# This file is machine-generated - editing it directly is not advised

[[Primes]]
git-tree-sha1 = "afccf037da52fa596223e5a0e331ff752e0e845c"
uuid = "27ebfcd6-29c5-5fa9-bf4b-fb8fc14df3ae"
version = "0.5.0"

2.5 Aktivace prostředí projektu

Prostředí projektu jde aktivovat několika způsoby, můžeme použít příkazovou řádku

$ julia --project=PROJECT_DIRECTORY

nebo, pokud jsme přímo v adresáři projektu

$ julia --project=@.

Alternativně lze použít Pkg mód a příkaz activate (s udáním adresáře, nebo . pro aktuální adresář).

V Pkg módu může být užitečný příkaz status, který nám vypíše aktuální stav projektu.

(MyProject) pkg> status
      Status `~/documents/fit/B231-BI-JUL/MyProject/Project.toml`
  [27ebfcd6] Primes v0.5.0

Nyní můžeme v rámci projektu implementovat co je potřeba. Často budeme chtít ale projekt sdílet s ostatními (například se studenty :-)).

Nejprve je nutno získat adresář s projektem (git repozitář, zip,...). Poté stačí spustit Julia ve správném prostředí a nainstalovat závislosti, v Pkg módu je pro to příkaz instantiate (tj. alternativně import Pkg; Pkg.instantiate()).


2.6 Cvičení: Projekt Möbius

Zkuste si stáhnout a zprovoznit ukázkový projekt MyProject s výpočtem Möbiovy funkce.

Prozkoumejte adresářovou strukturu, ověřte funkčnost.


3. Balíčky

Julia balíček ("knihovna" atp.) není nic jiného než trochu bohatší projekt s předepsanou strukturou. Doporučeným postupem při vytváření balíčku je použít balíček PkgTemplates, který vás procesem tvorby proveden. Z Python světa možná znáte analogický projekt Cookiecutter.

Nejprve nainstalujeme PkgTemplates, tedy v Pkg módu provedeme

(@v1.6) pkg> add PkgTemplates

Poté vytvoříme náš nový skvělý balíček (k dispozici je interaktivní průvodce, nebo lze šablonu ručně konfigurovat -- viz dokumentaci zmíněného balíčku):

julia> using PkgTemplates

julia> generate("DobbleExample")
Template keywords to customize:
[press: d=done, a=all, n=none]
   [X] user
   [X] authors
   [X] dir
   [X] host
   [X] julia
 > [X] plugins
Enter value for 'user' (String, default="kalvotom"): 
Enter value for 'authors' (Vector{String}, comma-delimited, default="Tomáš Kalvoda <tomas.kalvoda@fit.cvut.cz> and contributors"): 
Enter value for 'dir' (String, default="~/.julia/dev"): ./
Select Git repository hosting service:
   github.com
   gitlab.com
   bitbucket.org
 > Other
Enter value for 'host' (String, default="github.com"): gitlab.fit.cvut.cz
Select minimum Julia version:
   1.0
   1.1
   1.2
   1.3
   1.4
   1.5
 > 1.6
   Other
Select plugins:
[press: d=done, a=all, n=none]
   [ ] CompatHelper
   [X] ProjectFile
   [X] SrcDir
   [X] Git
   [X] License
   [X] Readme
   [X] Tests
   [ ] TagBot
   [ ] AppVeyor
   [ ] BlueStyleBadge
   [ ] CirrusCI
   [ ] Citation
 > [X] Codecov
   [ ] ColPracBadge
   [ ] Coveralls
   [ ] Develop
   [X] Documenter
   [ ] DroneCI
   [ ] GitHubActions
   [X] GitLabCI
   [ ] RegisterAction
   [ ] TravisCI
ProjectFile keywords to customize:
[press: d=done, a=all, n=none]
 > [ ] version
   [ ] None
SrcDir keywords to customize:
[press: d=done, a=all, n=none]
 > [ ] destination
   [ ] file
Git keywords to customize:
[press: d=done, a=all, n=none]
 > [ ] branch
   [ ] email
   [ ] gpgsign
   [ ] ignore
   [ ] jl
   [ ] manifest
   [ ] name
   [ ] ssh
License keywords to customize:
[press: d=done, a=all, n=none]
 > [ ] destination
   [ ] name
   [ ] path
Readme keywords to customize:
[press: d=done, a=all, n=none]
 > [ ] badge_off
   [ ] badge_order
   [ ] destination
   [ ] file
   [ ] inline_badges
Tests keywords to customize:
[press: d=done, a=all, n=none]
 > [ ] file
   [ ] project
Codecov keywords to customize:
[press: d=done, a=all, n=none]
 > [ ] file
   [ ] None
Documenter deploy style:
   NoDeploy
   TravisCI
 > GitLabCI
   GitHubActions
Documenter keywords to customize:
[press: d=done, a=all, n=none]
 > [ ] assets
   [ ] devbranch
   [ ] index_md
   [ ] logo
   [ ] make_jl
GitLabCI keywords to customize:
[press: d=done, a=all, n=none]
 > [ ] coverage
   [ ] extra_versions
   [ ] file
[ Info: Running prehooks
[ Info: Running hooks
  Activating environment at `~/documents/fit/B211-BI-JUL/DobbleExample/Project.toml`
    Updating registry at `~/.julia/registries/General`
  No Changes to `~/documents/fit/B211-BI-JUL/DobbleExample/Project.toml`
  No Changes to `~/documents/fit/B211-BI-JUL/DobbleExample/Manifest.toml`
Precompiling project...
  1 dependency successfully precompiled in 3 seconds
  Activating environment at `~/.julia/environments/v1.6/Project.toml`
  Activating new environment at `~/documents/fit/B211-BI-JUL/DobbleExample/docs/Project.toml`
   Resolving package versions...
    Updating `~/documents/fit/B211-BI-JUL/DobbleExample/docs/Project.toml`
  [e30172f5] + Documenter v0.27.10
    Updating `~/documents/fit/B211-BI-JUL/DobbleExample/docs/Manifest.toml`
  [a4c015fc] + ANSIColoredPrinters v0.0.1
  [ffbed154] + DocStringExtensions v0.8.6
  [e30172f5] + Documenter v0.27.10
  [b5f81e59] + IOCapture v0.2.2
  [682c06a0] + JSON v0.21.2
  [69de0a69] + Parsers v2.1.1
  [2a0f44e3] + Base64
  [ade2ca70] + Dates
  [b77e0a4c] + InteractiveUtils
  [76f85450] + LibGit2
  [56ddb016] + Logging
  [d6f4376e] + Markdown
  [a63ad114] + Mmap
  [ca575930] + NetworkOptions
  [de0858da] + Printf
  [3fa0cd96] + REPL
  [9a3f8284] + Random
  [ea8e919c] + SHA
  [9e88b42a] + Serialization
  [6462fe0b] + Sockets
  [8dfed614] + Test
  [4ec0a83e] + Unicode
   Resolving package versions...
    Updating `~/documents/fit/B211-BI-JUL/DobbleExample/docs/Project.toml`
  [8640a1c4] + DobbleExample v0.1.0 `..`
    Updating `~/documents/fit/B211-BI-JUL/DobbleExample/docs/Manifest.toml`
  [8640a1c4] + DobbleExample v0.1.0 `..`
  Activating environment at `~/.julia/environments/v1.6/Project.toml`
[ Info: Running posthooks
[ Info: New package is at /home/kalvin/documents/fit/B211-BI-JUL/DobbleExample
Template:
  authors: ["Tomáš Kalvoda <tomas.kalvoda@fit.cvut.cz> and contributors"]
  dir: "~/documents/fit/B211-BI-JUL"
  host: "gitlab.fit.cvut.cz"
  julia: v"1.6.0"
  user: "kalvotom"
  plugins:
    Codecov:
      file: nothing
    Documenter:
      assets: String[]
      logo: Logo(nothing, nothing)
      makedocs_kwargs: Dict{Symbol, Any}()
      canonical_url: PkgTemplates.gitlab_pages_url
      make_jl: "~/.julia/packages/PkgTemplates/VOMig/templates/docs/make.jl"
      index_md: "~/.julia/packages/PkgTemplates/VOMig/templates/docs/src/index.md"
      devbranch: nothing
    Git:
      ignore: String[]
      name: nothing
      email: nothing
      branch: "main"
      ssh: false
      jl: true
      manifest: false
      gpgsign: false
    GitLabCI:
      file: "~/.julia/packages/PkgTemplates/VOMig/templates/gitlab-ci.yml"
      coverage: true
      extra_versions: ["1.0", "1.6"]
    License:
      path: "~/.julia/packages/PkgTemplates/VOMig/templates/licenses/MIT"
      destination: "LICENSE"
    ProjectFile:
      version: v"0.1.0"
    Readme:
      file: "~/.julia/packages/PkgTemplates/VOMig/templates/README.md"
      destination: "README.md"
      inline_badges: false
      badge_order: DataType[Documenter{GitHubActions}, Documenter{GitLabCI}, Documenter{TravisCI}, GitHubActions, GitLabCI, TravisCI, AppVeyor, DroneCI, CirrusCI, Codecov, Coveralls, BlueStyleBadge, ColPracBadge]
      badge_off: DataType[]
    SrcDir:
      file: "~/.julia/packages/PkgTemplates/VOMig/templates/src/module.jl"
    Tests:
      file: "~/.julia/packages/PkgTemplates/VOMig/templates/test/runtests.jl"
      project: false

Po této akci vznikne následující adresářová struktura:

$ tree -a 
.
├── docs
│   ├── make.jl
│   ├── Manifest.toml
│   ├── Project.toml
│   └── src
│       └── index.md
├── .git
│   └── # CENSORED
├── .gitignore
├── .gitlab-ci.yml
├── LICENSE
├── Manifest.toml
├── Project.toml
├── README.md
├── src
│   └── DobbleExample.jl
└── test
    └── runtests.jl

Tuto šablonu jsem ještě lehce upravil a doplnil a naleznete ho v repozitáři v předmětové skupině.


3.1 Cvičení: Generátor Dobble karet

V uvedeném balíčku je velmi hrubý nástřel balíčku umožňujícího generovat Dobble kartičky. Během cvičení dokončíme implementaci a vybavíme balíček

  • generátorem dokumentace,
  • testy,
  • měřením pokrytí kódu testy.

Detaily k zadání naleznete v uvedeném repozitáři.

V implementaci si také vyzkušíme implementovat iterátor (přes všechny kartičky/přímky). K tomu je potřeba vytvořit metodu iterate.


3.2 Kam dál?


Reference

V tomto notebooku vycházíme z oficiální dokumentace modulů, z dokumentace Pkg.jl a dokumentace PkgTemplates.jl.

Dokumentaci generátoru dokumentace naleznete zde.