本日は
アドベントカレンダー 7 日目の記事です.
6 日目の記事
で Julia を導入したと思うのでそれを前提で話を進めます.
また書き忘れてましたが,環境変数として
JULIA_PROJECT=@. を追加しておいてください.
macOS, Linux であれば ~/.zshrc, ~/.bashrc などに
# Julia
export JULIA_PROJECT=@.
と記述しておけばOKです.
自作パッケージを作ってみる.
Julia のバージョンが 1.12 系以上を前提としています.workspace, main,-m などの機能を使うためです.
ここでは NumFactor というパッケージを作成し,ユーザが与えた整数を素因数分解する機能を持たせることにします.最終的には下記のようなことができることを目標にします.
$ julia --module NumFactor 12
Primes.Factorization(2 => 2, 3 => 1)
出力の結果は
$12 = 4 \times 3 = 2 ^ 2 \times 3 ^ 1$
の結果からわかるように素数とその冪のペアの系列を出してくれます.
Pkg.generate でパッケージの雛形を作る
各自の好きな作業ディレクトリに移動し Julia の REPL を開きます.
julia> using Pkg; Pkg.generate("NumFactor")
下記のような出力を得ます.
Generating project NumFactor:
NumFactor/Project.toml
NumFactor/src/NumFactor.jl
Dict{String, Base.UUID} with 1 entry:
"NumFactor" => UUID("a3f11fea-92d6-40ee-b6aa-501fe75866e8")
NumFactor という名前のディレクトリが作られます.tree コマンドでディレクトリの構造を見てみましょう.
$ tree NumFactor
NumFactor
├── Project.toml
└── src
└── NumFactor.jl
NumFactor ディレクトリ以下の階層では
-
Project.tomlというファイル -
srcディレクトリ
が作られます. Project.toml は Python の pyproject.toml や Rust の Cargo.toml に相当し依存パッケージなどを管理する役割を果たします.
$ cat Project.toml
name = "NumFactor"
uuid = "<UUID, 各自の環境で出力が異なる>"
version = "0.1.0"
authors = ["各自の名前"]
src ディレクトリ以下には NumFactor.jl というファイルが作られます.
$ cat src/NumFactor.jl
module NumFactor
greet() = print("Hello World!")
end # module NumFactor
この中に主要な機能を詰め込んでいきます.
以下作業ディレクトリは NumFactor ディレクトリ直下にいるとし話を進めます.
$ cd path/to/NumFactor
$ ls
Project.toml src/
Project.toml に依存パッケージを記述する.
NumFactor パッケージでは Primes が持っている factor 関数を利用することにします.この関数は整数を素因数分解する役割を果たします.そこで我々はNumFactorパッケージが Primes パッケージに依存することを示すために下記のコマンドを実行します.
$ julia -e 'using Pkg; Pkg.activate("."); Pkg.add("Primes")
これは次と同じです.
$ julia --project -e 'using Pkg; Pkg.add("Primes")
もっと言えば Julia の REPL で下記をすることと同じです.
julia> using Pkg; Pkg.activate("."); Pkg.add("Primes")
Project.toml に下記が追記されてることを確認します.
[deps]
Primes = "27ebfcd6-29c5-5fa9-bf4b-fb8fc14df3ae"
[compat]
Primes = "0.5.7"
Pkg.activate(".") や --project は Project.toml があるディレクトリを Julia の環境とみなしアクティベートすることをする操作です.Pythonで言えば source .venv/bin/activate をすることに近いでしょう.
ただ,毎回これをするのは煩雑なので環境変数として
JULIA_PROJECT=@.
をセットすることを勧めます.これをしておくことで Pkg.activate(".") や --project を省略できます.
src/NumFactor.jl を実装する
下記のように実装します.
module NumFactor
using Primes: factor
function @main(args)
if length(args) != 1
println("Usage: numfactor <number>")
return 0
end
n = parse(Int, args[1])
println(factor(n))
return 0
end
end # module NumFactor
function @main() ... end の箇所が
julia --module NumFactor をターミナルで実行した際に実行されるエントリーポイントです.
julia --module NumFactor 12 とすると
args 変数は ["12"] となります.一般に String型 を要素とする配列がやってきます.
ここでは引数は一つしか受け付けないという仕様にしておきます.そこでlength(args) がちょうど 1 でない場合は println("Usage: numfactor <number>") というメッセージを標準出力に出しておいてプログラムを終了します.
引数がちょうど引数である場合は要素の一番目を args[1] として取得し文字列を Int 型として解釈させます.(n = parse(Int, args[1]) の箇所に相当)
factor(n) で n を素因数分解してその結果を出力します.
$ julia --module NumFactor 12
Primes.Factorization(2 => 2, 3 => 1)
--module は長いので -m でも良いです.
$ julia -m NumFactor 12
Primes.Factorization(2 => 2, 3 => 1)
テストを書いてみる.
単純な CLI ですがテストを書いてみましょう.Project.toml を編集し下記のようにします
name = "NumFactor"
uuid = "<UUID 各自の環境に依存>"
version = "0.1.0"
authors = ["各自の名前"]
[deps]
Primes = "27ebfcd6-29c5-5fa9-bf4b-fb8fc14df3ae"
[compat]
Primes = "0.5.7"
# ここの部分を追加
[workspace]
projects = ["test"]
つまり手動で下記を追加します.
[workspace]
projects = ["test"]
この作法は Julia 1.12 から導入された workspace という機能です.
今までは Project.toml には extras と targets を指定する書き方でした:
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Test"]
ですが,Julia 1.12 Highlights によれば
Test-specific dependencies are now recommended to be specified using the workspace approach (a project file in the test directory that is part of the workspace defined by the package project file).
と書いてあるので今後はこの作法が推奨されるようです.
今後は workspace を使うことになるようです.
test/runtests.jl を作る
src と同じ階層に test ディレクトリを作ります.そして test 直下に runtests.jl という名前のファイルを作ります.これは従来の Julia と同じです.
$ ls
Project.toml src/
$ mkdir test
$ touch test/runtests.jl
$ touch test/Project.toml
workspace を使う作法は test ディレクトリ直下に Project.toml を作ってテストにのみ使う依存パッケージを登録する方法です.
$ cd test
$ ls
runtests.jl Project.toml
$ julia --project -e 'using Pkg; Pkg.add("Test")'
$ julia --project -e 'using Pkg; Pkg.develop(path="../")'
$ cat Project.toml
[deps]
NumFactor = "a3f11fea-92d6-40ee-b6aa-501fe75866e8"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[sources]
NumFactor = {path = ".."}
ここでは標準の Test とテスト対象の NumFactor を導入しました.
一旦 NumFactor ディレクトリに戻ります(重要).
そして次を実行します.
$ julia --project -e 'using Pkg; Pkg.test()'
次のような出力が出ます.
Status `~/tmp/NumFactor/test/Project.toml`
[a3f11fea] NumFactor v0.1.0 `.`
[8dfed614] Test v1.11.0
Status `~/tmp/NumFactor/Manifest.toml`
[18e54dd8] IntegerMathUtils v0.1.3
[a3f11fea] NumFactor v0.1.0 `.`
[27ebfcd6] Primes v0.5.7
[2a0f44e3] Base64 v1.11.0
[b77e0a4c] InteractiveUtils v1.11.0
[ac6e5ff7] JuliaSyntaxHighlighting v1.12.0
[56ddb016] Logging v1.11.0
[d6f4376e] Markdown v1.11.0
[9a3f8284] Random v1.11.0
[ea8e919c] SHA v0.7.0
[9e88b42a] Serialization v1.11.0
[f489334b] StyledStrings v1.11.0
[8dfed614] Test v1.11.0
Testing Running tests...
Testing NumFactor tests passed
何もテストを書いていないので 0 個のテストが通っています.
test/runtests.jl を充実させる.
例えば下記のようなもを作ってみましょう.
using Test
using NumFactor
@testset "main with a composite number" begin
original_stdout = stdout
(read_end, write_end) = redirect_stdout()
# NumFactor.@main ではなく NumFactor.main であることに注意する
return_code = NumFactor.main(["12"])
redirect_stdout(original_stdout)
close(write_end)
output = read(read_end, String)
@test return_code == 0
@test occursin("2 => 2", output)
@test occursin("3 => 1", output)
end
これは julia --module NumFactor 12 としたときの挙動をテストする様子です.出力に 2 => 2 などが含まれているかなどをチェックします.
このテストは TabNine のテスト自動生成機能が書いてくれましたが,実際に期待したテストを作ってくれたのでそれを採用しています.
もう少し充実させたものが下記のようになります.
using Test
using NumFactor
@testset "main with a prime number" begin
original_stdout = stdout
(read_end, write_end) = redirect_stdout()
return_code = NumFactor.main(["13"])
redirect_stdout(original_stdout)
close(write_end)
output = read(read_end, String)
@test return_code == 0
@test occursin("13 => 1", output)
end
@testset "main with a composite number" begin
original_stdout = stdout
(read_end, write_end) = redirect_stdout()
return_code = NumFactor.main(["12"])
redirect_stdout(original_stdout)
close(write_end)
output = read(read_end, String)
@test return_code == 0
@test occursin("2 => 2", output)
@test occursin("3 => 1", output)
end
@testset "main with no arguments" begin
original_stdout = stdout
(read_end, write_end) = redirect_stdout()
return_code = NumFactor.main([])
redirect_stdout(original_stdout)
close(write_end)
output = read(read_end, String)
@test return_code == 0
@test output == "Usage: numfactor <number>\n"
end
@testset "main with too many arguments" begin
original_stdout = stdout
(read_end, write_end) = redirect_stdout()
return_code = NumFactor.main(["10", "20"])
redirect_stdout(original_stdout)
close(write_end)
output = read(read_end, String)
@test return_code == 0
@test output == "Usage: numfactor <number>\n"
end
@testset "main with non-integer argument" begin
@test_throws ArgumentError NumFactor.main(["abc"])
end
@testset "main with floating-point argument" begin
@test_throws ArgumentError NumFactor.main(["12.3"])
end
これで一通りパッケージの作り方をマスターできたと思います.お疲れ様でした.
まとめ
- Julia 1.12 系の機能を使ってエントリーポイントを持つ簡単な機能を持つパッケージを作りました
- テストを作るために workspace 機能を使いました
以上お役に立てれば幸いです.