Moduły
Do tej pory używaliśmy gotowych modułów poprzez using
lub import
. Jak zrobić własny moduł?
Najprostszy moduł
Moduły w Julii mają bardzo prostą strukturę
module Stuffs
# kod
struct Stuff
#kod
end
end
zwykle kod modułu wrzucony do pojedynczego pliku byłby bardzo nieczytelny, więc jest rozbity na pliki zawierające pewne fragmenty
module Stuffs
include("structs.jl")
include("functions.jl")
#...
end
Te funkcje i zmienne, które chcemy udostępnić w przypadku wywołania using
muszą być wyeksportowane
module Stuffs
export Stuff, movestuff
struct Stuff
x::Float64
end
function movestuff(s, x)
if checkmove(s, x)
return s.x + x
else
return s.x
end
end
function checkmove(s, x)
if s.x + x > 0
return true
else
return false
end
end
end
Struktura Stuff
i funkcja movestuff
będą wyeksportowane do przestrzeni globalnej w przypadku wywowałania using Stuffs
. Natomiast funkcja checkmove
nie będzie wyekportowana i aby jej użyć będzie potrzebna składnia zawierająca nazwę modułu - Stuffs.checkmove(s, x)
.
Praca z projektem
Mały projekt
Typowa praca z niewielkim, lokalnym projektem może składać się z
Stworzenia modułu np.
A.jl
module A
export do_things
function do_things()
print("working...")
#...
println("done!")
end
end
Stworzenia pliku z kodem testującym modułu np.
testA.jl
module testA
include("A.jl")
using .A
A.do_things()
#lub
function runtests()
A.do_things()
end
end
Takie objęcie testującego kodu w moduł powoduje, że ewentualne zmienne, funkcje itp. nie są umieszczane w globalnym kontekście - kod jest czystszy i jest mniejsza szansa konfliktów nazw.
Następnie pracujemy w REPL
julia> include("testA.jl")
julia> testA.runtests()
julia> ...
Nowe pomysły i poprawki wprowadzamy do A.jl lub testA.jl (w zależności od potrzeb).
Zmieniony kod ładujemy wywołując
include("testA.jl")
Revise
Pakiet Revise
ułatwia pracę z projektami śledząc zmiany w pakietach i na bieżąco je ładując. Dzięki temu unikamy konfliktów raz zdefiniowanych struktur, modułów, konieczności przeładowywania środowiska (i utraty skompilowanych funkcji) itp.
Zwykle podczas pracy nad projektem, zanim zaczniemy cokolwiek innego ładujemy moduł Revise
.
julia> using Revise
Możemy teraz pracować nad projektem, a wywołanie
julia> include("testA.jl")
powinno uruchamiać nasz kod testujący z wszelkimi zmianami we wszystkich modułach itp.
Menadżer pakietów
Pkg
jest menadżerem pakietów w Julii, który po pierwsze instaluje i usuwa pakiety. Ale, co równie istnotnie, zarządza całym systemem niezależnych środowisk (environments
) przypisanych do różnych projektów. Jeżeli nie używamy przez dłuższy czas jakiegoś projektu, lub dostajemy go od kogoś, zazwyczaj okazuje się, że pakiety są nie w tej wersji, która jest potrzebna lub ich już nie ma. Każdy projekt może zależeć od różnych pakietów i różnych ich wersji, środowiska służą do rozwiązywania tego problemu - mają swoje niezależne wewnętrzne zależności i wersje. Uruchomienie środowiska przywraca taki stan, jaki został przypisany danemu projektowi. Pozwala to też wprowadzać poprawki niezależnie od oficjalnych kanałów. Jeżeli dany pakiet ma jakiś krytyczny błąd i chcemy go poprawić, możemy to zrobić wewnątrz jego środowiska i używać, a poprawkę niezależnie od tego wysłać do autorów (gdy zostanie wprowadzona, można oczywiście globalnie aktualizować dany pakiet).
Podstawy
Po przejściu w REPL w tryb menadżera ]
możemy dodawać, aktualizować i usuwać pakiety poleceniami
pkg> add Pakiet
pkg> update Pakiet
pkg> rm Pakiet
alternatywą jest
julia> using Pkg
julia> Pkg.add("Pakiet")
julia> Pkg.update("Pakiet")
julia> Pkg.rm("Pakiet")
Środowiska
Po naciśnieciu ]
otrzymujemy prompt w postaci
(@v1.9) pkg>
Informacja (@v1.9) pokazuje nam aktywne środowisko (w tym przypadku globalne).
Polecenie
pkg> status
powinno pokazać nam wersje pakietów w tym środowisku, np.
[c7e460c6] ArgParse v1.1.4
[6e4b80f9] BenchmarkTools v1.3.1
[336ed68f] CSV v0.10.3
[5ae59095] Colors v0.12.8
[a93c6f00] DataFrames v1.3.2
[31c24e10] Distributions v0.25.52
[0337cf30] GRUtils v0.7.1
[f67ccb44] HDF5 v0.16.4
...
Przechodzimy teraz do odpowiedniego miejsca, gdzie chcemy założyć projekt (np. ~/workspace/Project) i tworzymy nowe środowisko
pkg> activate Project
lub
using Pkg
Pkg.activate("Project")
Możemy sprawdzić teraz stan nowego projektu
pkg> status
(lub Pkg.status()
, dalej polecenia będą już tylko w trybie menadżera).
Jeżeli chcemy dodać zależności to będą one dodawane do projektu
pkg> add Test
pkg> status
Jeżeli chcemy wrócić do naszego projektu lub czyjegoś (np. uzyskanego przez git clone
) to możemy użyć środowiska do znalezienia się w odpowiednich wersjach wszystkich zależności
pkg> activate .
pkg> instantiate
Nowe pakiety
Tworzenie nowych pakietów w najprostszej wersji polega na wydaniu polecenia
pkg> generate Projekt
Aczkolwiek wygodniejsze i pełniejsze pakiety można tworzyć za pomocą PkgTemplates
using PkgTemplates
t = Template(interactive=true)
t("Projekt")
wersja interaktywna pozwala ustalić jakie parametry chcemy zmienić w standardowym wzorcu. Zwykle będziemy chcieli podać nazwę użytkownika (zwykle github lub innego serwisu kontroli wersji) i ścieżkę lokalną (domyślna jest w JULIA_DIR/dev).
t = Template(;user="k.a.miernik@gmail.com", dir="~/workspace")
t("Projekt")
Jeżeli przejdziemy teraz do utworzonego projektu to możemy sprawdzić jego strukturę
shell>cd Projekt
shell> tree .
.
├── LICENSE
├── Manifest.toml
├── Project.toml
├── README.md
├── src
│ └── Projekt.jl
└── test
└── runtests.jl
Szablon zawiera pliki:
Project.toml - informacje o nazwie projektu, unikatowym numerze UUID, autorze i zależnościach
Manifest.toml - dokładny stan wszystkich zależności, wraz z Project.toml umożliwia dokładne odtworzenie środowiska
README.md, LICENSE
src/Projekt.jl - główny plik projektu:
```julia module Projekt
# Write your package code here. end
```
test/runtests.jl - plik z testami projektu
```julia using Projekt using Test
@testset "Projekt.jl" begin # Write your tests here. end
```
Testy można uruchomić poprzez
julia> include("test/runtests.jl")
pkg> test
Testy
Projekty często składają się z bardzo wielu elementów powiązanych ze sobą w jawny i niejawny sposób. Zmiana funkcjonalności lub poprawki w jednej części mogą wywołać niezamierzone efekty w innej części projektu. Jedną z metod kontroli i pracy nad projektami jest technika tworzenia oprogramowania w oparciu o testy (Test driven development). Elementy tej metody można wykorzystać także i mniejszych projektach. Generalny schemat postępowania jest następujący:
Tworzymy nowy test (np. funkcji, struktury, itp.), który przy znanych warunkach powinien podać znaną odpowiedź.
Test początkowo powinien dawać wynik negatywny
Piszemy odpowiednią funkcję w taki sposób, aby test dawał wynik pozytywny
Wszystkie testy powinny teraz działać pozytywnie
Nowy fragment kodu refaktoryzujemy: porządkujemy, rozbijamy na mniejsze funkcje, oczyszczamy z powtarzających się elementów, przenosimy w odpowiednie miejsce i dokumentujemy
Testy nadal powinny działać poprawnie
Powtarzamy od punktu 1 dla kolejnej funkcjonalności
Testy w Julii mają bardzo swobodną i elastyczną strukturę. Mogą być umieszczone w dowolnym miejscu, ale plik test/runtest.jl
jest automatycznie tworzony przez generator pakietów i jest wygodnym miejscem.
Test składa się z zestawów @testset
, z których każdy składa się z testów jednostkowych @test
sprawdzających czy wyrażenie zwraca prawdę czy fałsz. Wewnątrz środowiska @testset
można swobodnie tworzyć zmienne, używać pętli itp. Testy również mogą być używane w pętlach, mogą być zagniedżone.
Jeżeli testów jest dużo warto rozbić je na mniejsze pliki, które mogą być załączane w pliku runtest.jl
Poniższy przykład testów pochodzi z projektu symulującego detektory promieniowania jonizującego. Jednym z elementów projektu są zagadnienia geometryczne: jaka jest odległość między punktami, obiektami, czy dany punkt znajduje się wewnątrz danego obiektu itd.
# geometry_tests.jl
@testset "Distances" begin
ax = Line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0])
ay = Line([0.0, 0.0, 0.0], [0.0, 1.0, 0.0])
az = Line([0.0, 0.0, 0.0], [0.0, 0.0, 1.0])
lx = Line([0.0, 0.0, 1.0], [1.0, 0.0, 1.0])
ly = Line([0.0, 0.0, 1.0], [0.0, 1.0, 1.0])
lz = Line([0.0, 1.0, 0.0], [0.0, 1.0, 1.0])
for l1 in [ax, ay, az]
for l2 in [ax, ay, az]
@test distance(l1, l2) == 0.0
end
for l2 in [lx, ly, lz]
@test distance(l1, l2) == 1.0
end
end
end
@testset "Inside" begin
s1 = Sphere([0.0, 0.0, 0.0], 1.0)
@test isin([0.0, 0.0, 0.0], s1) == true
@test isin([0.0, 0.0, 1.0], s1) == false
@test isin([1.0, 1.0, 1.0], s1) == false
end
# runtest.jl
using NucPhysSim
using Test
@testset "All tests" begin
include("geometry_tests.jl")
include("ray_tests.jl")
include("particle_tests.jl")
include("physics_tests.jl")
end
Zadanie
Stworzyć szkic projektu za pomocą PkgTemplates. W katalogu test utworzyć plik z danymi wejściowymi "sample.csv"
x,y,z
0.0,1.0,2.0
0.0,2.0,1.0
1.0,0.0,2.0
1.0,2.0,0.0
2.0,1.0,0.0
2.0,0.0,1.0
Stworzyć testy:
funkcji czytającej dane, który sprawdzi czy wczytane zostały wszystkie punkty (długość struktury DataFrame)
funkcji wyliczających: sumę, średnią i odchylenia standardowego dla kolumn x, y i z
Następnie napisać odpowiednie funkcje, tak, aby wszystkie testy były zaliczone pozytywnie.