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

  1. Stworzenia modułu np. A.jl

module A

export do_things

function do_things()
    print("working...")
    #...
    println("done!")
end

end
  1. 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.

  1. Następnie pracujemy w REPL

julia> include("testA.jl")
julia> testA.runtests()
julia> ...
  1. Nowe pomysły i poprawki wprowadzamy do A.jl lub testA.jl (w zależności od potrzeb).

  2. 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:

  1. Tworzymy nowy test (np. funkcji, struktury, itp.), który przy znanych warunkach powinien podać znaną odpowiedź.

  2. Test początkowo powinien dawać wynik negatywny

  3. Piszemy odpowiednią funkcję w taki sposób, aby test dawał wynik pozytywny

  4. Wszystkie testy powinny teraz działać pozytywnie

  5. Nowy fragment kodu refaktoryzujemy: porządkujemy, rozbijamy na mniejsze funkcje, oczyszczamy z powtarzających się elementów, przenosimy w odpowiednie miejsce i dokumentujemy

  6. Testy nadal powinny działać poprawnie

  7. 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:

  1. funkcji czytającej dane, który sprawdzi czy wczytane zostały wszystkie punkty (długość struktury DataFrame)

  2. 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.

CC BY-SA 4.0 Krzysztof Miernik. Last modified: December 23, 2023. Website built with Franklin.jl and the Julia programming language.