本日は
アドベントカレンダーの2日目です
プログラミング言語 Julia の導入をします.
インストール
適当なシェル環境で julia
と入力したら下記のような対話環境を出力することを目標にします.
% julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.8.3 (2022-11-14)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia>
概ね下記の方法1, 方法2のどちらかで行えば良いです.個人的には方法2 を使っています.
方法1: 手動でバイナリを入手する方法
バイナリは https://julialang.org/ から入手することができます. ダウンロードしたバイナリのパスを PATH
環境変数に追加すれば動作します. 具体的なやり方は使用する OS 毎の説明がある https://julialang.org/downloads/platform/ をご覧ください.
方法2: Cross-platform installer を用いる方法
juliaup を用いた場合
juliaup
を使うと新しい Julia のバージョンがリリースされた際にわざわざバイナリを手動で入手することなく juliaup
コマンドを経由して使用するバージョンを切り替えることができます.Linux 環境では juliaup の README の通りにすると導入することができます:
curl -fsSL https://install.julialang.org | sh
以下のスクリーンショットは GCP の Cloud Shell で行ったものをキャプチャしたものです.
特にこだわりがなければ Proceed with installation
のまま Enter を押して進めます.
julia
を入力して使えることを確認します.
特定のバージョン,例えば LTS 扱い の 1.6 系を導入する場合は
$ # 1.6 をインストール
$ juliaup add 1.6
$ # バージョンは切り替わらない
$ julia --version
julia version 1.8.3
$ # 暫定的に使う場合
julia +1.6
$ # 1.6 を既定にする場合
$ juliaup default 1.6
$ # 元に戻す場合
$ juliaup default 1.8
とはいえ 1.6
とか 1.8
というような数値は覚えたくないと思うので
$ # 安定リリース版を使う場合
$ juliaup default release
$ # LTS 版を使う場合
$ juliaup default lts
を覚えておけば良いです.特にこだわりがなければ安定最新版(release) の方を使うと良いです.
Windows の場合
Windows 環境ですと Microsoft Store からインストールすると juliaup
というコマンドと julia
が使えるようになります.
winget
を使っても入手できるようです.winget
は Windows 11 ではデフォルトで入っているようなので導入が楽になっています.
winget install julia -s msstore
jill を用いた方法
普段は Python を使っていて juliaup
の導入時に curl ...
コマンドが覚えられない. そんな人は jill
を使うのもありでしょう:
$ pip3 install jill
$ jill install 1.8
REPL で遊ぶ
インストールが完了できれば julia
コマンドによって下記のような対話環境が得られます.これを REPL (read-eval-print loop) と呼びます.
Julia は Python のようにインデントに関してセンシティブではないので巷にあるサンプルコードをコピーして REPL にペーストしてコードを実行させることができます. 例えば下記の julia>
も含んだコードは REPL 上で動かすことを念頭に置いています.julia>
の部分を丸ごとコピーして貼り付けても REPL 側でよしなに処理してくれます.
julia> msg = "Hello"
"Hello"
julia> println(msg)
Hello
julia> (.*)(1:9, (1:9)') # ブロードキャストで九九表を作成
9×9 Matrix{Int64}:
1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81
Julia の REPL は便利な機能が含まれています.時間のある方はYouTubeにある Julia のワークショップが参考になります.
こんなこともできます.
簡易逆引き
これをするにはどすればいい?というのを簡単にですがまとめます. (スペースの都合で余分な改行は省略しています.)
Hello World
julia> msg = "Hello World" # "Hello World" を `msg` に紐づける
"Hello World"
julia> msg * " " * "Julia" # 文字列の結合は `*` を使う
"Hello World Julia"
julia> println(msg)
Hello World
julia> print(msg); print(" "); print("Julia")
Hello World Julia
julia> name = "Goma"; "Hello $name"
"Hello Goma"
-
println
出力の最後に改行を追加します.print
は改行を含めません. -
#
の隣には Python と同様にコメントを書くことができます. -
"$(name)"
でname
の値を補間することができます(string-interpolation):
- セミコロンを挟むことで複数行のコードを1行にまとめることができます.
簡易的な電卓として
$x=3$ のときに $2x+5$, $x^2$ を計算したい場合はほぼそのままコードに落とし込むことができます.
julia> x = 3
3
julia> 2x + 5
11
julia> 2 * x + 5
11
julia> x ^ 2 # x の二乗を計算
9
julia> 5 / 2
2.5
julia> div(5, 2) # これは 5 を 2 で割った商を出す
2
数式を数式らしくコードにしたい
Julia は Unicode 文字を変数名に採用することができ入力を支援する機能がそなわっています. 円周率 $\pi$ は pi
でも使うことができますが \pi
のあとTABキーを入力することで $π$ を使うことができます. $\LaTeX$ を触ったことがある方は習熟しやすいでしょう.
julia> θ = π/6 # \theta
0.5235987755982988
julia> sin(2θ) ≈ 2sin(θ)*cos(θ)
true
↑の ≈
は何?
≈
は isapprox
関数のエイリアスです. \approx
+ TAB で入力ができます.
それを調べるには REPL で ?≈
を入力するとヘルプモードにアクセスします:
例えば下記のコードは Julia の文法として意味のあるコードです.
julia> 3 ∈ [1, 2, 3] ∩ [3, 4, 5]
true
julia> x₁ = 1; x₂ = 2; x₃ = 3
3
julia> 𝐱 = [x₁, x₂, x₃]
3-element Vector{Int64}:
1
2
3
関数とかループとか条件分岐とか知りたい
乱数を振って円周率を計算する例で全部例示できます.
julia> function calcpi(N)
cnt = 0
for i in 1:N
x = rand()
y = rand()
if x^2 + y^2 < 1
cnt += 1
end
end
4cnt/N
end
calcpi (generic function with 1 method)
julia> calcpi(10000)
3.14
-
function calcpi(N)... end
でcaclpi
という名前の関数名を定義できます.-
return
キーワード がないですが最後に評価した式の値を返す仕様になっています.もちろんつけても OK です. つけるかつけないかは個々の趣味やチーム毎のコーディング規約に依存します.
-
-
calcpi(10000)
で呼び出せます.for i in 1:N ... end
で繰り返し処理を書きます. - 条件分岐は
if 条件式 ... end
で書けます.- 今回の場合は
x^2 + y^2 < 1 && (cnt += 1)
と1行で書くこともできます.
https://docs.julialang.org/en/v1/manual/control-flow/#Short-Circuit-Evaluation - 条件式の値は型が Bool 値になること要請します.
julia> 1 && true ERROR: TypeError: non-boolean (Int64) used in boolean context
- 今回の場合は
ライブラリを使ってみたい.
導入
パッケージと呼ぶことが多いです. 例を示すために Example.jl パッケージ を導入してみましょう.
julia> using Pkg; Pkg.add("Example")
Pkg.add("Example")
で Example.jl
パッケージをインストールすることができます.Pkg
は Julia に組み込みされているパッケージマネージャ(の機能を扱うパッケージ)です.
https://pkgdocs.julialang.org/v1/
https://docs.julialang.org/en/v1/stdlib/Pkg/
使う
using Example
を実行すると Example.jl
パッケージオブジェクトに提供されている hello
関数を使うことができます.
julia> using Example
julia> Example.hello("Goma")
"Hello, Goma"
julia> hello("Goma")
"Hello, Goma"
using Example
と実行することで Example.hello
とするだけでなく hello
を使うことができています. Example.jl
内部で定義している名前・識別子を現在の(グローバルの)名前空間に導入します.どのような名前を知るには Example.jl パッケージを読む必要があります.
export hello, domath
とあるので using Example
によって hello
と(実は) domath
を現在の Julia のセッションに導入していることがわかります.
Python ユーザーにとってはちょっと気持悪いと感じるかもしれませんが, 慣れてくると少量のタイプ量で多くの機能にアクセスできる体験の良さがわかってきます. むしろ from ... import ...
がめんどいと感じるようになるはずです.
どうしても導入される名前を制御したい
何かしらの理由で domath
のみを導入したい場合, 特定の関数や値・型がどのパッケージから由来するのか明示したい場合は using Example
の代わりに using Example: domath
をすればOKです. hello
単体では使うことができないことが確認できるでしょう. 極論 using Example: Example
とすれば Example
以外の名前を導入を抑えることができます.
ひとまずは using <パッケージ名>
でOK
Julia では Example.hello
みたいに仰々しくモジュール(ここでは Example
のこと)名を付与することはしません.開発者は多重ディスパッチを使って同じ名前のメソッドを実装しユーザーはシンプルで柔軟なインターフェースを使って機能にアクセスすることができます. ユーザーとして Julia を使うのであれば using Plots
とか using Distributions
とか using Images
のように using BenchmarkTools
みたいに using
を積極的に使うと良いです.
追記: 2024 年 ただし,パッケージを開発する際は多くのパッケージに依存すると後からメンテナンスする際にどの識別子がどのパッケージからやってきたかを判断するのは現時点でも(手動で検索しなければいけない程度には)難しいので,パッケージ開発時には using Something: myfunc
のような書き方を身につけておくと良いでしょう.3ヶ月後のソースコードは他人が書いたもののように記憶が消し去ります.
もう少し実用的なライブラリを触りたい.
REPL でグラフを描く: UnicodePlots.jl
関数のグラフを書いてみましょう.
julia> using UnicodePlots
julia> plt = lineplot(sin)
julia> lineplot!(plt, cos)
julia> lineplot!(plt, x-> 1 - 0.01x^2, name="Quadratic")
using UnicodePlots
によって lineplot
, lineplot!
を使うことができます.$\sin$, $\cos$, $f(x) = 1 - 0.001x^2$ を描画できます.
関数の末尾に !
がある場合入力引数に影響を与えますよというプログラマの意図を読む側に伝える目的を持っています. 例えば $\sin$ を描画後 $\cos$ を描画する際にグラフオブジェクト plt
の状態が変わります. 文法的に強制されているわけではないですがコーディング規約として用いられています.
x-> 1 - 0.01x^2
の代わりに f(x) = 1 - 0.001x^2
と f
を定義して lineplot!(plt, f)
でも OK です.
関数だけでなく座標の列を入力させることもできます.
julia> lineplot([1,2,3],[3,1,2])
┌────────────────────────────────────────┐
3 │⠣⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠱⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠀⠈⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠀⠀⠀⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠀⠀⠀⠀⠱⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠀⠀⠀⠀⠀⠘⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠀⠀⠀⠀⠀⠀⠈⢢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠│
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠊⠀│
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠣⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠊⠁⠀⠀⠀│
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠔⠁⠀⠀⠀⠀⠀⠀│
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠣⡀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⡄⠀⠀⠀⠀⡠⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
1 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢆⣀⠔⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
└────────────────────────────────────────┘
⠀1⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀3⠀
Julia の良さの一つに多重ディスパッチがあります.lineplot
の名前さえ覚えておけばあとはなんとなく「こうすればいいだろ」という想像力を持って入力を与えておけば開発者が用意した適切なメソッドを呼び出してくれます.@which lineplot(sin)
によって lineplot
関数に紐づくどの実装(メソッド)を呼ぶのかを人間に教えてくれます:
julia> @which lineplot(sin)
lineplot(f::Function; kw...) in UnicodePlots at ~/.julia/packages/UnicodePlots/Z7FG6/src/interface/lineplot.jl:223
julia> @which lineplot([1,2,3],[3,1,2])
lineplot(x::AbstractVector, y::AbstractVector) in UnicodePlots at ~/.julia/packages/UnicodePlots/Z7FG6/src/interface/lineplot.jl:65
これにより初見でも特定の識別子がどこからやってきたのかをある程度知ることができます.詳細には VSCode などでデバッガを起動して掘り下げていくことになります.
乱数発生をさせたい: Distributions.jl
一様乱数や標準正規分布に従う乱数を発生させるのはとても簡単です. 各々 rand
, randn
で生成できます.
julia> using UnicodePlots
julia> xs = rand(100000);
julia> ns = randn(100000);
末尾にセミコロンを入れると冗長な出力を抑えることができます. UnicodePlots.jl
でサンプルのヒストグラムを可視化できます:
julia> histogram(xs)
julia> histogram(ns)
Distributions.jl は幅広い分布の種類をカバーします.
julia> using StatsPlots, Distributions
julia> unicodeplots() # おまじない
julia> plot(Exponential(0.1), linecolor=:red)
julia> plot(Laplace(0, 1), linecolor=:green)
StatsPlots.jl は Distributions.jl で定義されている分布の密度関数を可視化してくれます.
また下記の点にも注意しましょう:
julia> @which rand(100)
rand() in Random at /Applications/Julia-1.8.app/Contents/Resources/julia/share/julia/stdlib/v1.8/Random/src/Random.jl:278
julia> @which rand(Laplace(0, 1), 100)
rand(s::Sampleable, dims::Int64...) in Distributions at ~/.julia/packages/Distributions/gggmX/src/genericrand.jl:22
前者は前述の通り一様分布を後者はラプラス分布 Laplace(0, 1)
に従うサンプルを 100 個生成するメソッドの定義場所を示しています. using Distributions
をすることによって rand
ができること(機能が増えていること)がわかります. 実は Distributions.jl は 標準で使える rand
を拡張して機能を提供しています.これも Julia の多重ディスパッチの恩恵です.
シードを付与して乱数の発生を制御したい
実験の再現性などで擬似乱数の生成を決定的にしたい場合は
擬似乱数生成アルゴリズムとシード値を明示的に指定し擬似乱数アルゴリズムのインスタンスを rand
に入力すればOKです.標準ライブラリの Random が提供する Xoshiro
を使ってみましょう.
julia> using Random, Distributions
julia> rand(Xoshiro(0), Bernoulli(0.2), 5)
5-element Vector{Bool}:
0
1
0
1
0
julia> rand(Xoshiro(0), Bernoulli(0.2), 5)
5-element Vector{Bool}:
0
1
0
1
0
パフォーマンス計測
BenchmarkTools.jl パッケージがあります.
julia> @benchmark minimum(rand(1000))
julia> @benchmark minimum($(rand(1000)))
以下の例はランダムに生成した配列の最小値を求める関数のベンチマークをとっています.
前者は試行毎に rand(1000)
が実行されます.従って乱数生成の時間と minimum
関数の計算の両方を含んでいます. 後者は rand(1000)
の値を補間して minimum
側の計算にフォーカスしてベンチマークをとっています.
を眺めておくと良いでしょう.
@ほにゃらら
はなに?
マクロの呼び出しをしています. マクロはコード断片を入力とし適切に変換されたコード生成します. 例えば Python での f-string の機能を使うと f"{x=}"
x=<xの値>
が出力されます. Julia では @show
マクロを使うと実現できます.
julia> x = 3
3
julia> @show x
x = 3
3
@macroexpand
というマクロを使うとマクロがどのようなコードを生成するのかを観察することができます:
julia> @macroexpand @show x
quote
Base.println("x = ", Base.repr(begin
#= show.jl:1047 =#
local var"#13#value" = x
end))
var"#13#value"
end
大雑把にいえば x
というコードの断片を入力することで下記のようなコードを生成・評価していることを意味しています.
println("x = ", x)
x
関数の1回あたりの実行時間を計測する @elapsed
マクロについても調べてみましょう:
julia> @elapsed rand(100)
2.973e-6
julia> @macroexpand @elapsed rand(100)
quote
#= timing.jl:380 =#
$(Expr(:meta, :force_compile))
#= timing.jl:381 =#
local var"#21#t0" = Base.time_ns()
#= timing.jl:382 =#
rand(100)
#= timing.jl:383 =#
(Base.time_ns() - var"#21#t0") / 1.0e9
end
大雑把にいえば下記のようなコードを生成・評価していることがわかります.
s = time_ns()
rand(100) # これが入力したコードの断片
e = time_ns()
(e - s) / 1.0e9
配列
Julia は(デフォルトでは) 1 始まりです. Python や C/C++ のプログラムにどっぷり使ってると Julia に移植する際に微妙な混乱を引き起こします.
Python のコードで表現できる場合は 0
を使う場合は配列の最初の位置の値を取得する目的が多いと思います.その場合は下記のような表記で要素のアクセスを抽象化できます.
julia> arr = [-1,-2,-3]
3-element Vector{Int64}:
-1
-2
-3
julia> arr[1]
-1
julia> arr[begin] # arr[end]
-1
julia> first(arr) # last(arr)
-1
- 剰余系の完全代表系を 1 始まりにしたい場合は
mod
ではなくmod1
にすると良いです. - どうしても 1 始まりが気持ち悪い.コードを line by line で忠実に移植する必要があれば OffsetArrays.jl を使って添字の原点を調整することができます:
julia> using OffsetArrays: Origin
julia> oa = Origin(0)([1,2,3])
3-element OffsetArray(::Vector{Int64}, 0:2) with eltype Int64 with indices 0:2:
1
2
3
julia> oa[0]
1
julia> oa[begin]
1
julia> first(oa)
1
- 多次元配列で役立つと思われるパッケージも併せて紹介しておきます:
まとめ
書いたらキリがないですが,入門時に知っておくと良いことを書いておきました.
Python 入門系は Jupyter Notebook の環境を構築して云々してようやくスタートに立てますが,Julia は REPL だけで色々遊ぶことができます.最近はパッケージのインストールの失敗で諦めるという事象は減ってきてると思うので特殊な環境でない入門では限り困らないと思います.
必要に応じて help?>
を使ったりドキュメントを読んだり,ソースコードを追いかけたり Web 上の解説を読むと良いでしょう.
ちなみに Julia は 2009 年に開発がスタートし 2012 年に公開されました.ということは公開から10周年です.ちょうどいい節目を逃す前に記念にトライしてはいかがでしょうか?