このチュートリアルはJuliaの機能を最速で学ぶためのものです。 Juliaは科学技術計算を得意とするプログラミング言語で、スクリプト言語のように手軽に書くことができるのにC言語やFortranにも負けない実行速度を誇ります。 文法や機能はPythonなど他のスクリプト言語とかなり共通しているので、別のプログラミング言語の知識があれば基本的な機能はすぐに習得できるでしょう。
他のスクリプト言語と比較した、Juliaの特異な点をいくつか挙げてみると、
というようなものが挙げられます。 それゆえ、今までのようにパフォーマンスが必要な部分をC言語など他の言語で書く必要はなく、すべてJuliaで書けます。 実際、文字列を含むJuliaの標準ライブラリはほとんどJuliaで書かれており、十分なパフォーマンスを持ちます。
ここで使用するJuliaのバージョンは2018年9月の最新版であるv1.0(もしくはv0.7)です。 それでは、早速Juliaを学んでいきましょう!
まずはJuliaのコードをざっと見てみましょう。配列をソートするクイックソートのコードです。
quicksort(xs) = quicksort!(copy(xs))
quicksort!(xs) = quicksort!(xs, 1, length(xs))
function quicksort!(xs, lo, hi)
if lo < hi
p = partition(xs, lo, hi)
quicksort!(xs, lo, p - 1)
quicksort!(xs, p + 1, hi)
end
return xs
end
function partition(xs, lo, hi)
pivot = div(lo + hi, 2)
pvalue = xs[pivot]
xs[pivot], xs[hi] = xs[hi], xs[pivot]
j = lo
@inbounds for i in lo:hi-1
if xs[i] <= pvalue
xs[i], xs[j] = xs[j], xs[i]
j += 1
end
end
xs[j], xs[hi] = xs[hi], xs[j]
return j
end
partition (generic function with 1 method)
どうでしょう。PythonやRubyをやったことがる人なら初見でも大体の意味が分かるのではないでしょうか。 まるで擬似コードのような、スッキリした文法です。
関数定義・分岐・反復などの構文はそれぞれfunction ... end
, if ... end
, for ... end
, while ... end
のようにキーワードで始まりend
で終わります。 ちょうどRubyと同じような感じです。 インデントはPythonのように必要ではありませんが、4スペースでひとつのインデントを表すのが慣習です。
また、6行目のp = partition(xs, lo, hi)
で分かるように変数の宣言や型の指定は通常必要ありません。
18行目の@inbounds
というのが気になるかもしれません。
@
マークで始まるこの表記はマクロ呼出しと言われ、コードを書き換えたりJuliaのコンパイラに最適化のヒントを与えることができます。
ここで使われている@inbounds
は添字アクセス(xs[i]
など)のチェックを省き、少し計算を高速化できますが、配列の範囲外にアクセスした時はセグメンテーション違反などを起こし停止する可能性があります。
こうしたJuliaで書かれたコードはJuliaのLLVMベースのJITコンパイラによりコンパイルされ、C言語などで書いたコードとそれほど変わらない速度で実行できます。
試しに整数の小さい配列をソートしてみると、うまく行っています。
quicksort([3, 6, 2, 4, 5, 1])
6-element Array{Int64,1}: 1 2 3 4 5 6
浮動小数点でも動きます。
quicksort([-2.1, 3.4, 5.0, -1.2, 3.1, 0.1])
6-element Array{Float64,1}: -2.1 -1.2 0.1 3.1 3.4 5.0
1千万要素もあるような浮動小数点数の配列のソートも一瞬です。
using Random: seed!
seed!(0xdeadbeef)
xs = randn(10_000_000)
@time quicksort(xs)
1.211873 seconds (6 allocations: 76.294 MiB, 4.84% gc time)
10000000-element Array{Float64,1}: -5.201821821719352 -5.050624909871284 -4.983261442674312 -4.974985552832985 -4.893857076120602 -4.864517550048943 -4.85004091215009 -4.811551905035468 -4.785936931795382 -4.758123564483724 -4.751953182949212 -4.751136270866197 -4.700641597803617 ⋮ 4.643556492102544 4.664239936011457 4.682333119886756 4.685774543279274 4.695170058903736 4.710676182931005 4.824126039132785 4.889103328580121 4.920689915842902 4.976832282177101 5.16998796753511 5.293639671892019
ここでひとつ注目すべきことは、quicksort
関数の定義時に引数の型を指定していなかったにも関わらず、整数にも浮動小数点数にも適用できるということです。
実は、関数の最初の実行時にそれぞれの型に応じて高速なコードを生成しています。
この機能のおかげで、Juliaでは関数の型を一切指定しなくても十分なパフォーマンスが得られます。
quicksort
は、数値にかぎらず以下の様な文字や文字列でも適用できます。
quicksort(['B', 'A', 'D', 'E', 'C'])
5-element Array{Char,1}: 'A' 'B' 'C' 'D' 'E'
quicksort(["Bob", "Alice", "Dave", "Eve", "Charlie"])
5-element Array{String,1}: "Alice" "Bob" "Charlie" "Dave" "Eve"
Juliaの変数名はとても自由です。英字やアンダースコア(_
)から始まる英数字/アンダースコアの他に、UTF8の多様な文字が使えます。
使えるもの:
xyz
_foo3_
π
f′
使えないもの:
33xyz
などの数値から始まるものfor
, function
, end
など予約語x.y
, x:y
などの予約されている記号を使ったものx
などが使えるのはもちろんですが、
x = 100
100
η
のようなギリシャ文字や漢字も使えます。
η = 0.01
0.01
漢字変数 = η
0.01
ここまで見てきたように、変数は特別に宣言せずとも初期化と同時に使用できます。
変数には、その影響するソースコードの範囲であるスコープという概念があります。
function
やfor
などで始まり、end
で終わるほとんどのブロックは新たな変数のスコープを作ります。for ... end
がスコープを作るのはPythonなどと動作が異なりますので注意が必要です。
以下の例では、変数xx
はfor ... end
の内側のみのスコープを持つため、その外でxx
にアクセスすると変数未定義の例外が発生しています。
for i in 1:10
xx = i
end
xx
UndefVarError: xx not defined Stacktrace: [1] top-level scope at In[10]:4
例外的にスコープを作らないのはif ... end
とbegin ... end
です。すなわち、if ... end
やbegin ... end
の内側で定義した変数にはその外側でもアクセスできます。
if true
yy = 10
end
yy
10
Juliaの値は型を持ちます。Juliaでは動的に型がつき、様々な機能と密接に関わってきます。
整数型は符号有無と8bit, 16bit, 32bit, 64bit, 128bitの組み合わせの10種類、それに加えてBool
型とBigInt
型で合計12種類あり、それぞれ符号付き64bitはInt64
や符号なし(unsigned)32bitはUInt32
など一貫性のある型名がつけられています。
浮動小数点数の型も16bit, 32bit, 64bitとBigFloat
型で合計4種類があります。型名はFloat64
などのように精度の数値が最後についています。
BigInt
型とBigFloat
型はそれぞれ任意精度の整数と浮動小数点数です。
Int64
Int64
Float64
Float64
他には複素数のComplex{T}
型があります。
T
というのは型パラメータ(type parameter)で、実部と虚部の数値の型を指定します。ちょうどC++のテンプレートやHaskellの型変数(type variable)のようなものです。
Complex{Float64}
Complex{Float64}
特殊な例として、円周率のような定数はIrrational
型として定義されています。
π
π = 3.1415926535897...
isa(π, Irrational) # isa(x, typ)は値xが型typの値であるかどうかを返す関数
true
科学計算のために作られているJuliaは、このように豊富な数値の型を持つ点が魅力のひとつです。
大抵の場合、何らかの値を作るリテラルは他の言語と同じです。
Int
型: 1
, 42
, -4
Float64
型: 3.14
, -2.1
, 6.0221413e+23
Complex{T}
型: 3 + 2im
, 1.1 - 9.1im
Rational{T}
型: 3 // 2
, 355 // 113
Char
型: 'a'
, '樹'
String
型: "deadbeef"
, "漢字"
, """Triple Quote String"""
Bool
型: true
, false
Nothing
型: nothing
実際のソースコードではInt
型を目にすることが多いですが、これは環境によってInt32
またはInt64
のエイリアスになっています。
型は以下のようにtypeof
で確認できます。
typeof(42)
Int64
typeof(42.0)
Float64
typeof(3 // 2)
Rational{Int64}
typeof(2 + 3im)
Complex{Int64}
typeof('A')
Char
typeof('漢')
Char
typeof("deadbeef")
String
typeof("漢字")
String
typeof(true)
Bool
typeof(nothing)
Nothing
また,ある値がある型であるかはisa
関数で確認できます。
isa(1, Int)
true
1 isa Int # 中置もできる
true
Juliaでは1次元配列をベクトル(Vector
)、2次元配列を行列(Matrix
)とよびます。
ベクトル(1次元配列)は[x,y,...]
で表現します。
[1,2,3]
3-element Array{Int64,1}: 1 2 3
添字のアクセスはカギ括弧([]
)を使います。添字は1
から始まり、配列の長さ分で終わります。
# 5要素のベクトル
xs = [1,2,3,4,5]
5-element Array{Int64,1}: 1 2 3 4 5
xs[1]
1
xs[5]
5
値の更新も標準的な構文で可能です。
xs[2] = 200
200
xs
5-element Array{Int64,1}: 1 200 3 4 5
配列を末尾からアクセスするときはend
が使えます。
xs[end]
5
xs[end] == xs[5]
true
xs[end-1] == xs[4]
true
ある範囲を切り出すにはn:m
というような書き方ができます。
xs[2:3]
2-element Array{Int64,1}: 200 3
xs[end-2:end]
3-element Array{Int64,1}: 3 4 5
型もちょっと見てみましょう。[x,y,...]
で作られるのはArray
型の値です。
以下の{Int64,1}
の意味は後の型システムのところで説明します。
typeof([1,2,3])
Array{Int64,1}
また、[1,2,3]
はVector
型の値でもあります。これはisa
関数でチェックできます。
isa([1,2,3], Vector)
true
行列は[a b c; d e f]
のように書けます。
[1 2 3; 4 5 6]
2×3 Array{Int64,2}: 1 2 3 4 5 6
x = [1 2 3;
4 5 6]
2×3 Array{Int64,2}: 1 2 3 4 5 6
添字でのアクセスも見ておきましょう。
x[1,2]
2
x[2,end]
6
Vector
の時と同様にMatrix
型であることを確認しておきます。
isa([1 2 3; 4 5 6], Matrix)
true
配列の要素数はlength
で取得します。
length([1,2,3])
3
length([1 2 3; 4 5 6])
6
タプル(組)は(x,y,...)
です。
(1,2,3)
(1, 2, 3)
typeof((1,2,3))
Tuple{Int64,Int64,Int64}
タプルもベクトル同様、添字でのアクセスが出来ます
(1,2,3)[2]
2
(1,2,3)[end]
3
タプルの括弧は、曖昧性がなければ省略できます。
1,2,3
(1, 2, 3)
配列の大きさはsize
関数で得られますが、タプルとして返されます。
size([1,2,3])
(3,)
size([1 2 3; 4 5 6])
(2, 3)
タプルとベクトルはよく似ていますが、内部の構造や動作は大きく異なります。
まず、タプルは不変(immutable)ですが、ベクトルや配列は可変(mutable)です。 したがって、一度作ったタプルはそれ以降変更できませんが、配列では可能です。
また、タプルはメモリーの割当が起きないことがあるため、オブジェクトの生成コストが極めて小さいです。
Juliaには値の範囲を表す範囲型も用意されています。start:stop
のように書くことで、start
からstop
まで、両端を含む範囲を表現します。
1:10
1:10
'a':'z'
'a':1:'z'
これはfor
ループを書くときや、配列や文字列から一部分を切り出す際に用いられます。
for i in 3:6
println(i)
end
3 4 5 6
x = [1,2,3,4,5,6]
6-element Array{Int64,1}: 1 2 3 4 5 6
x[3:6]
4-element Array{Int64,1}: 3 4 5 6
start:step:stop
のように書くことで、ステップ幅を指定することもできます。
for i in 0:10:90
println(i)
end
0 10 20 30 40 50 60 70 80 90
ステップ幅に-1
を指定すれば、逆順の範囲も作れます。
for i in 5:-1:1
println(i)
end
5 4 3 2 1
Juliaでは辞書ももちろん用意されています。
x = Dict("foo" => 1, "bar" => 2)
Dict{String,Int64} with 2 entries: "bar" => 2 "foo" => 1
x["foo"]
1
Juliaにおいて、型は非常に重要な意味を持ちます。 PythonやRubyのようにJuliaは動的型付け言語ですが、オブジェクト指向のプログラミング言語での型の使い方とはかなり異なっています。 ここでは、Juliaの型システムを理解し、その後に出てくる型の定義や動的ディスパッチの前提知識を固めましょう。
Juliaの値はひとつの 具体的な 型を持ちます。一部例外を除いて、型が自動的に別の型にキャストされることはありません
(一部例外とは、1 + 1.5
などの数値計算とコンストラクタです http://julia.readthedocs.org/en/latest/manual/conversion-and-promotion/)。
ここで、 具体的な (concrete) とわざわざ述べたのは、Juliaにはこれと対極をなす 抽象的な (abstract) 型があるためです。 適切な訳語がない、ここでは原文通り具体的な型を「具体型」、抽象的な型を「抽象型」と表記します。
最も大きな違いは、具体型はインスタンス化可能な一方で抽象型はインスタンス化ができない点です。
したがって、任意のx
に対して、typeof(x)
の結果は必ず具体型になります。
抽象型は具体型や他の抽象型のsupertypeとして機能し、型間のsubtype/supertypeの関係性は木構造のグラフをとります。
さらに、具体型は他の型のsupertypeにはなれませんので、必然的にグラフの葉が具体型に、内部ノードが抽象型となります。
このグラフの根にあるのがAny
という抽象型です。
Juliaでユーザーが型を定義するとデフォルトでこのAny
型のsubtypeになります。
具体例で確認しましょう。Int64
とInt32
は共に具体型で、Integer
という抽象型のsubtypeになっています。
具体型はisconcretetype
、subtype/supertypeの関係は<:
という関数で確認できます。
isconcretetype(Int64)
true
isconcretetype(Int32)
true
isconcretetype(Integer)
false
Int64 <: Integer
true
Int32 <: Integer
true
他には、Bool
型などもInteger
型のsubtypeです。
Bool <: Integer
true
全ての型はsupertypeを一つだけ持ち、抽象型は複数のsubtypeを持つことができます。
それぞれsupertype
とsubtypes
という関数があるので、少しみてみましょう。
supertype(Bool)
Integer
subtypes(Integer)
3-element Array{Any,1}: Bool Signed Unsigned
subtypes(Signed)
6-element Array{Any,1}: BigInt Int128 Int16 Int32 Int64 Int8
supertypeを辿って行くと、最終的にはAny
型にたどり着きます。
次のようなtracetype
関数で検証してみましょう。
function tracetype(t)
print(t)
while t != supertype(t)
print(" <: ")
t = supertype(t)
print(t)
end
println()
end
tracetype (generic function with 1 method)
どこから抽象型が共通になってるかなどにも注目して眺めてみて下さい。
tracetype(Int64)
Int64 <: Signed <: Integer <: Real <: Number <: Any
tracetype(Bool)
Bool <: Integer <: Real <: Number <: Any
tracetype(BigInt)
BigInt <: Signed <: Integer <: Real <: Number <: Any
tracetype(Float64)
Float64 <: AbstractFloat <: Real <: Number <: Any
tracetype(String)
String <: AbstractString <: Any
数値の型に関しては、以下の図のようになっています。
[1,2,3]
は以下の様な型です。
typeof([1,2,3])
Array{Int64,1}
これは、[1,2,3]
がInt64
型を要素とする1
次元の配列(ベクトル)という意味になります。一般化すると、Array{T,N}
型はT
型を要素とするN
次元の配列です。
このように、Juliaでは型が別の型や値をパラメータとして持つことができます。
以下の例で、型パラメータに注目して確認してください。
[1.0, 2.0, 3.0]
3-element Array{Float64,1}: 1.0 2.0 3.0
typeof([1.0, 2.0, 3.0])
Array{Float64,1}
[1 2 3]
1×3 Array{Int64,2}: 1 2 3
typeof([1 2 3])
Array{Int64,2}
[1 2 3; 4 5 6]
2×3 Array{Int64,2}: 1 2 3 4 5 6
typeof([1 2 3; 4 5 6])
Array{Int64,2}
自分で型を作るのも簡単です。
以下のようにstruct
またはmutable struct
に続けて型名を書き、フィールドを定義します。
mutable struct Person
name::String
age::Int
end
struct Location
x::Float64
y::Float64
end
デフォルトコンストラクタがありますので、即座にインスタンス化できます。
インスタンスのフィールドへはドット(.
)でアクセスできます。
person = Person("ヤマダ田中", 34)
Person("ヤマダ田中", 34)
person.name, person.age
("ヤマダ田中", 34)
mutable struct
で作られた型のフィールド値は更新できます。
person.age += 1
35
person
Person("ヤマダ田中", 35)
loc = Location(1.0, 2.0)
Location(1.0, 2.0)
loc.x, loc.y
(1.0, 2.0)
struct
で作られた型のフィールド値は更新できません。
loc.x += 3.0
type Location is immutable Stacktrace: [1] setproperty!(::Location, ::Symbol, ::Float64) at ./sysimg.jl:19 [2] top-level scope at In[94]:1
既に何度か出てきますが、関数の定義は
function <関数名>(<引数>, ...)
<関数本体>
end
を使います。
返り値はreturn
を使いますが、最後に評価された式が自動的に返り値になるので省略可能です。
# 2次元空間でのpとq間のユークリッド距離
function dist(p, q)
dx = p[1] - q[1]
dy = p[2] - q[2]
sqrt((dx)^2 + (dy)^2)
end
dist (generic function with 1 method)
dist((0, 0), (3, 4))
5.0
もう一つ、別の方法として以下の「代入」のような形も使えます。一行で済むような 簡単な関数を定義するときに便利です。
dist(p, q) = sqrt((p[1] - q[1])^2 + (p[2] - q[2])^2)
dist (generic function with 1 method)
dist((0, 0), (3, 4))
5.0
オプション引数やキーワード引数、可変長引数もJuliaでは使えます。
# オプション引数: '='
ktop(xs, k=3) = sort(xs)[1:k]
ktop (generic function with 2 methods)
ktop([1,5,3,2,6])
3-element Array{Int64,1}: 1 2 3
ktop([1,5,3,2,6], 2)
2-element Array{Int64,1}: 1 2
# キーワード引数: ';' '='
function ktop(xs; k=3, rev=false)
sort(xs, rev=rev)[1:k]
end
ktop (generic function with 2 methods)
ktop([1,5,3,2,6], k=2)
2-element Array{Int64,1}: 1 2
ktop([1,5,3,2,6], k=4, rev=true)
4-element Array{Int64,1}: 6 5 3 2
function pathlength(p, q, rs...)
len = dist(p, q)
for r in rs
len += dist(q, r)
q = r
end
return len
end
pathlength (generic function with 1 method)
pathlength((0, 0), (1, 1))
1.4142135623730951
pathlength((0, 0), (1, 1), (1, 2))
2.414213562373095
pathlength((0, 0), (1, 1), (1, 2), (0, 0))
4.650281539872885
それぞれに特徴的な記号をまとめておきます。
オプション引数 | `=` | `xs, k=3` |
---|---|---|
キーワード引数 | `;` `=` | `xs; k=3` |
可変長引数 | `...` | `x, xs...` |
ここで、Juliaでの関数名の慣習を確認しておきます。 ドキュメントや標準ライブラリの関数から推奨される関数の命名法は以下の様なものになります (括弧内は標準ライブラリにある関数の具体例です)。
get
, readline
)_
)は、なるべく使わない (readdlm
, findmin
)open
, serialize
)Bool
を返す関数はis...
やhas...
という名前を使う (isempty
, haskey
)!
をつける (push!
, sort!
)一番最初の例を振り返ってみると、この形の関数定義を使っていました。
quicksort(xs) = quicksort!(copy(xs))
quicksort!(xs) = quicksort!(xs, 1, length(xs))
quicksort!
のように、関数名の最後に!
をつけるのは引数を変更するときの慣習的な方法です。
関数の引数は基本的に参照渡しのようになるため、Array
などmutableな型の値は関数内で変更できます。
実はJuliaでは、関数がとる引数の型を<引数>::<型>
のように指定することもできます。これは他のスクリプト言語にはあまりみられない特徴です。
function add(x::Int, y::Int)
return x + y
end
add (generic function with 1 method)
型が合えば関数を呼び出すことができます。
add(1, 2)
3
しかし型が合わないと、呼び出すこともできません。
add(1.0, 2.0)
MethodError: no method matching add(::Float64, ::Float64) Stacktrace: [1] top-level scope at In[111]:1
ここで、次の関数を定義してみましょう
function add(x::Float64, y::Float64)
return x + y
end
add (generic function with 2 methods)
すると、先ほど呼び出せなかった方の組み合わせで呼び出せるようになります
add(1.0, 2.0)
3.0
しかも、これは元の関数の上書きではありません。元のInt
, Int
の組み合わせでも呼び出せます。
add(1, 2)
3
このように、引数の型に合わせて呼び出すメソッドを変える仕組みを多重ディスパッチと呼びます。 以下の例で、これがどのように動くのかを確認して下さい。
function foo(x::Int)
println("foo Int: $x")
end
function foo(x::Int, y::Int)
println("foo Int Int: $x $y")
end
function foo(x::Float64, y::Float64)
println("foo Float64 Float64: $x $y")
end
function foo(x::Int, y::Float64)
println("foo Int Float64: $x $y")
end
foo (generic function with 4 methods)
foo(1)
foo Int: 1
foo(1, 2)
foo Int Int: 1 2
foo(1.0, 3.14)
foo Float64 Float64: 1.0 3.14
foo(1, 3.14)
foo Int Float64: 1 3.14
型の指定はInt64
やFloat64
のような具体型に限りません。
より高位の抽象型を使うこともできます。
add(BigInt(1), BigFloat(1)) # これにマッチするメソッドはまだない
MethodError: no method matching add(::BigInt, ::BigFloat) Stacktrace: [1] top-level scope at In[120]:1
x isa Number
かつy isa Number
になれば、以下のメソッドを呼び出せますので、具体型を使っていたときに比べ適用範囲がぐっと広がります。
function add(x::Number, y::Number)
return x + y
end
add (generic function with 3 methods)
add(BigInt(1), BigFloat(1)) # 今度は呼び出せる
2.0
add(1, π), add(π, 1.0), add(true, false) # 様々な型の組み合わせが使える
(4.141592653589793, 4.141592653589793, 1)
Juliaのコンストラクタは通常の関数と同様に定義でき、多重ディスパッチも利用できます。 コンストラクタは以下の2種類に分けられます。
外部コンストラクタは、他の外部コンストラクタや内部コンストラクタを呼び出すことでオブジェクトを作ることができます。 最終的に、全てのオブジェクトは内部コンストラクタを経由して作られるため、内部コンストラクタで最終的な値のチェックなどを実現できます。
それでは、具体例を見てみましょう。
以下のRGB
型はRed-Green-Blueの3原色を指定して色を表現する型です。
それぞれの色は8bitの符号なし整数でエンコーディングしています。
しかし、色を作る際は8bitで表現できない値が与えられる可能性があるため、不正な値が与えられれば例外を投げるようにしたいです。
これを実現するため、内部コンストラクタで与えられた値のチェックをしています。
内部コンストラクタは、構文的には(mutable) struct ... end
の内部で定義された関数です。
内部コンストラクタの特別な点は、new
関数を呼び出すことで、その型のオブジェクトを作ることができるところにあります。
struct RGB
r::UInt8
g::UInt8
b::UInt8
function RGB(r, g, b)
@assert 0 <= r <= 255
@assert 0 <= g <= 255
@assert 0 <= b <= 255
return new(r, g, b)
end
end
RGB(1, 2, 3)
RGB(0x01, 0x02, 0x03)
300など8bitで表現できない色が与えられたら、@assert
マクロが例外を投げます。
RGB(2, 300, 2)
AssertionError: 0 <= g <= 255 Stacktrace: [1] RGB(::Int64, ::Int64, ::Int64) at ./In[124]:8 [2] top-level scope at In[126]:1
デフォルトコンストラクタはnew
関数を呼び出すだけの内部コンストラクタで、プログラマが明示的に内部コンストラクタを定義するとデフォルトコンストラクタは作られません。
ある1つの値を与えたら、R,G,Bすべてに同じ値を設定する簡便なコンストラクタが欲しいかもしれません。 その場合は以下のように外部コンストラクタを使うと便利です。
RGB(x) = RGB(x, x, x)
RGB
外部コンストラクタは構文的には(mutable) struct ... end
の外部で定義された関数です。ここではnew
関数は使えず、内部コンストラクタや他の外部コンストラクタを呼び出すことでオブジェクトを作ります。
これは、他の外部コンストラクタを呼び出す外部コンストラクタの例です。
RGB() = RGB(0)
RGB
多重ディスパッチのお陰で、以下の3つのコンストラクタはすべて使うことができます。
RGB(), RGB(10), RGB(10, 20, 30)
(RGB(0x00, 0x00, 0x00), RGB(0x0a, 0x0a, 0x0a), RGB(0x0a, 0x14, 0x1e))
ここではJuliaで見られる特徴的な構文をざっと見て行きます。
変数や関数の前に数値を置くことで、その値との積を表現できます。
x = 2.1
2.1
2x
4.2
4x^2 + 3x - 2
21.94
x = range(0, stop=1, length=50);
4sin.(2x) - 3cos.(4x)
50-element Array{Float64,1}: -3.0 -2.8267897373141215 -2.6339373233305805 -2.4219133558638326 -2.1913188324640127 -1.9428824987789626 -1.677457339318942 -1.3960162317568687 -1.0996467914829426 -0.7895454385442837 -0.4670107243016668 -0.13343596009188374 0.20969880513734385 ⋮ 6.996871635711436 6.996458834488029 6.969421660503003 6.9159064004967 6.836202476105568 6.730740627863947 6.60009021313547 6.444955635692603 6.266171930354854 6.064699531632904 5.841618260670547 5.598120569893563
[]
内にfor
ループを書き、ベクトルや行列などの配列を作ることができます。
[x for x in 1:4]
4-element Array{Int64,1}: 1 2 3 4
[x * y for x in 1:4, y in 1:5]
4×5 Array{Int64,2}: 1 2 3 4 5 2 4 6 8 10 3 6 9 12 15 4 8 12 16 20
最後にif
をつけることでフィルターをかけることもできます。
[x for x in 1:10 if isodd(x)]
5-element Array{Int64,1}: 1 3 5 7 9
辞書も可能です。
Dict(c => i for (i, c) in enumerate('a':'z'))
Dict{Char,Int64} with 26 entries: 'n' => 14 'f' => 6 'w' => 23 'd' => 4 'e' => 5 'o' => 15 'h' => 8 'j' => 10 'i' => 9 'k' => 11 'r' => 18 's' => 19 't' => 20 'q' => 17 'y' => 25 'a' => 1 'c' => 3 'p' => 16 'm' => 13 'z' => 26 'g' => 7 'v' => 22 'l' => 12 'u' => 21 'x' => 24 'b' => 2
行列を転置するにはシングルクォート('
)を末尾につけるだけです。
x = [1 2 3; 4 5 6]
2×3 Array{Int64,2}: 1 2 3 4 5 6
x'
3×2 LinearAlgebra.Adjoint{Int64,Array{Int64,2}}: 1 4 2 5 3 6
transpose
関数でも転置できます。
transpose(x)
3×2 LinearAlgebra.Transpose{Int64,Array{Int64,2}}: 1 4 2 5 3 6
一時的に使う関数などを、名前を付けずに作ることができます。
x -> 2x
#12 (generic function with 1 method)
map(x -> 2x, [1,2,3])
3-element Array{Int64,1}: 2 4 6
第一引数に関数を取れる関数では、do ... end
を使うことで無名関数を後置することもできます。
map([1,2,3]) do x
2x
end
3-element Array{Int64,1}: 2 4 6
この機能は、ファイルを開いて処理をし、終わったら自動的にファイルを閉じるといった場面で多用されます。
open("sample.txt") do io
for (n, line) in enumerate(eachline(io))
println("$n: $line")
end
end
1: one 2: two 3: three
"..."
のような文字列リテラルの前に識別子を起き、その文字列を元に様々な操作を行うことができます。
以下は、r"..."
で正規表現オブジェクトを作る例です。
r"\w-\d \w+"
r"\w-\d \w+"
match(r"\w-\d \w+", "B-2 Spirit")
RegexMatch("B-2 Spirit")
バージョン番号を作るのにも使われます。
v"1.2.3"
v"1.2.3"
バッククウォートを使って外部コマンドを作成し、run
関数で実行できます。
`ls -la`
`ls -la`
run(`ls -la`)
total 1568 drwxr-xr-x 15 kenta staff 510 Sep 3 19:11 . drwxr-xr-x 47 kenta staff 1598 Aug 30 13:50 .. -rw-r--r--@ 1 kenta staff 6148 Dec 21 2015 .DS_Store drwxr-xr-x 14 kenta staff 476 Sep 3 18:57 .git -rw-r--r-- 1 kenta staff 20 Dec 21 2015 .gitignore drwxr-xr-x 7 kenta staff 238 Jul 24 2017 .ipynb_checkpoints -rw-r--r-- 1 kenta staff 576346 Aug 12 13:20 Juliaクックブック.ipynb -rw-r--r-- 1 kenta staff 126376 Sep 3 19:11 Julia高速チュートリアル.ipynb -rw-r--r-- 1 kenta staff 1100 Jul 4 2017 README.md drwxr-xr-x 6 kenta staff 204 Jul 8 2017 data drwxr-xr-x 4 kenta staff 136 Dec 21 2015 images -rw-r--r-- 1 kenta staff 167 Jul 2 2017 optimization.jl -rw-r--r-- 1 kenta staff 14 Apr 29 2015 sample.txt -rw-r--r-- 1 kenta staff 54968 Jul 2 2017 連続最適化.ipynb -rw-r--r-- 1 kenta staff 16114 Jul 24 2017 並行・並列計算.ipynb
Process(`ls -la`, ProcessExited(0))
run(`cat -n sample.txt`)
1 one 2 two 3 three
Process(`cat -n sample.txt`, ProcessExited(0))
Juliaには、Juliaのプログラムを使ってJuliaのコードを書き換えることができるメタプログラミングという機能があります。JuliaではLisp言語の影響から、この機能をマクロとも呼んでいます。 これを使うと、書くコードの量を劇的に減らしたり、普通の関数では実現できないような特異な働きをさせることができます。
Lispなどでは一般的に使われるマクロですが、他の言語にはあまり見られず、あったとしても自分で定義して使うことは稀でしょう。このチュートリアルでは既に定義されているマクロを使用してみて、マクロではどのようなことが可能になるのかを見ることにします。
@show
¶@show
マクロは、変数の値を確認する際に大変便利なマクロです。
これを使うと、コードのある部分で変数が何だったのかを瞬時に知ることができます。
x = 3.14
3.14
@show x
x = 3.14
3.14
挿入ソート(insertion sort)の動作を確認するために、@show
マクロを使ってみましょう。
insertionsort(xs) = insertionsort!(copy(xs))
function insertionsort!(xs)
@show xs
for i in 2:length(xs)
j = i
while j > 1 && xs[j-1] > xs[j]
xs[j-1], xs[j] = xs[j], xs[j-1]
j -= 1
end
@show xs
end
xs
end
insertionsort! (generic function with 1 method)
これを実行してみると、ベクトルが左から順にソートされていく様子がよくわかるのではないでしょうか。
insertionsort([6,5,3,1,8,7,2,4])
xs = [6, 5, 3, 1, 8, 7, 2, 4] xs = [5, 6, 3, 1, 8, 7, 2, 4] xs = [3, 5, 6, 1, 8, 7, 2, 4] xs = [1, 3, 5, 6, 8, 7, 2, 4] xs = [1, 3, 5, 6, 8, 7, 2, 4] xs = [1, 3, 5, 6, 7, 8, 2, 4] xs = [1, 2, 3, 5, 6, 7, 8, 4] xs = [1, 2, 3, 4, 5, 6, 7, 8]
8-element Array{Int64,1}: 1 2 3 4 5 6 7 8
@assert
¶@assert
マクロは、簡便な不定条件のチェックに便利なマクロです。
x = 1
1
条件が満たされるときは、何もしませんが、
@assert x == 1
条件に違反すると、例外を投げます。
@assert x == 2
AssertionError: x == 2 Stacktrace: [1] top-level scope at In[158]:1
エラーメッセージは自分で書くこともできます。
@assert x == 2 "xが$(x)ですよ! xは2じゃないとダメです!"
AssertionError: xが1ですよ! xは2じゃないとダメです! Stacktrace: [1] top-level scope at In[159]:1
@time
¶@time
マクロは、関数やコード片の実行時間とメモリ使用量を測るのに便利なマクロです。
先ほど定義したinsertionsort
の@show
を消して、quicksort
とパフォーマンスを比較してみましょう。
insertionsort(xs) = insertionsort!(copy(xs))
function insertionsort!(xs)
#@show xs
@inbounds for i in 2:length(xs)
j = i
while j > 1 && xs[j-1] > xs[j]
xs[j-1], xs[j] = xs[j], xs[j-1]
j -= 1
end
#@show xs
end
xs
end
insertionsort([1.0, 0.0])
2-element Array{Float64,1}: 0.0 1.0
小さい配列だとinsertionsort
とquicksort
が同程度に速いようですが、
xs = randn(10);
GC.gc()
@time for _ in 1:100000; insertionsort(xs); end
0.016046 seconds (100.00 k allocations: 15.259 MiB)
GC.gc()
@time for _ in 1:100000; quicksort(xs); end
0.015160 seconds (100.00 k allocations: 15.259 MiB)
大きい配列ではquicksort
の方が断然高速です。
xs = randn(10000);
GC.gc()
@time insertionsort(xs);
0.053030 seconds (6 allocations: 78.359 KiB)
GC.gc()
@time quicksort(xs);
0.001151 seconds (6 allocations: 78.359 KiB)
Juliaを使う人の多くは、数値計算を目的としていると思います。 ここで、標準ライブラリで使える便利な関数を紹介していきます。
[1,2,3]
のようにリテラルで直接配列を確保する以外にも、配列を確保する便利な関数が多数あります。
0
で埋められた配列が欲しい場合は、zeros
を使います。
基本的には、zeros(<型>, <サイズ>)
という書き方をします。
x = zeros(Float64, 4)
4-element Array{Float64,1}: 0.0 0.0 0.0 0.0
x = zeros(Float64, (4, 3))
4×3 Array{Float64,2}: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
x = zeros(Float64, (4, 3, 2))
4×3×2 Array{Float64,3}: [:, :, 1] = 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 [:, :, 2] = 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
同様に1
で埋められた配列が欲しい場合はones
です。
ones(Float64, 4)
4-element Array{Float64,1}: 1.0 1.0 1.0 1.0
LinearAlgebraモジュールにあるI
という定数は,単位行列のように振る舞います。
using LinearAlgebra
zeros(3, 3) + 4I
3×3 Array{Float64,2}: 4.0 0.0 0.0 0.0 4.0 0.0 0.0 0.0 4.0
もうちょっと複雑な初期化をしたい場合は、内包表記を使うのが便利でしょう。
[Float64(i > j) for i in 1:4, j in 1:3]
4×3 Array{Float64,2}: 0.0 0.0 0.0 1.0 0.0 0.0 1.0 1.0 0.0 1.0 1.0 1.0
特に初期化が必要ない場合は、Array
のコンストラクタを呼ぶこともできます。
Array{Float64}(undef, (3, 4))
3×4 Array{Float64,2}: 4.94066e-324 4.94066e-324 4.94066e-324 4.94066e-324 4.94066e-324 0.0 0.0 0.0 9.88131e-324 0.0 4.94066e-324 0.0
Float64
型はよく使うので、省略可能です。
ones((3, 4))
3×4 Array{Float64,2}: 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
ベクトルや配列の和や積は、数値の時のように行えます。
x = [1.0, 2.0, 3.0]
3-element Array{Float64,1}: 1.0 2.0 3.0
x .+ 1.0
3-element Array{Float64,1}: 2.0 3.0 4.0
x .- 1.0
3-element Array{Float64,1}: 0.0 1.0 2.0
x * 5
3-element Array{Float64,1}: 5.0 10.0 15.0
5x
3-element Array{Float64,1}: 5.0 10.0 15.0
x / 2
3-element Array{Float64,1}: 0.5 1.0 1.5
ベクトルどうしの和も計算できます。
x + 4x
3-element Array{Float64,1}: 5.0 10.0 15.0
0.3 * (x .- 1) + 0.7 * (x .+ 1)
3-element Array{Float64,1}: 1.4 2.3999999999999995 3.4
ひとつ気をつけなければいけないことは、ベクトルの要素ごとの積(アダマール積)は、.*
を使わなければならないということです。これは、*
がベクトルや行列自体の積に使われているためです。
x * 2x # 2つの列ベクトルの積は定義されていない
DimensionMismatch("Cannot multiply two vectors") Stacktrace: [1] *(::Array{Float64,1}, ::Array{Float64,1}) at /Users/osx/buildbot/slave/package_osx64/build/usr/share/julia/stdlib/v0.7/LinearAlgebra/src/deprecated.jl:566 [2] top-level scope at In[183]:1
x .* 2x # 2つの列ベクトルの要素毎の積は定義されている
3-element Array{Float64,1}: 2.0 8.0 18.0
要素ごとの除算も同様です。
2x ./ x
3-element Array{Float64,1}: 2.0 2.0 2.0
内積
dot(x, x)
14.0
行列とベクトルの積
A = [Float64(i + j) for i in 1:2, j in 1:3]
2×3 Array{Float64,2}: 2.0 3.0 4.0 3.0 4.0 5.0
A * x
2-element Array{Float64,1}: 20.0 26.0
行列の積
A' * A
3×3 Array{Float64,2}: 13.0 18.0 23.0 18.0 25.0 32.0 23.0 32.0 41.0
行列の積は次のように*
を省略しても書けます。
A'A
3×3 Array{Float64,2}: 13.0 18.0 23.0 18.0 25.0 32.0 23.0 32.0 41.0
一次線形方程式 $A x = b$ の解 $x$
b = [20.0, 26.0]
A \ b
3-element Array{Float64,1}: 1.0000000000000142 2.000000000000001 2.9999999999999902
すべての名前が一つの場所にあると、関数名などが衝突して予期せぬ動作をすることがあります。 特に、Juliaのように多重ディスパッチがあるときは問題が複雑になりかねません。 そういった問題を回避するためにも、名前空間を分けるJuliaのモジュールシステムを理解することは重要です。
Juliaでは、モジュールはmodule ... end
で明示的に定義します。
module
とend
で囲まれた部分のコードは、他の名前空間とは別の名前空間になります。
つまり、型や関数名は他のモジュールと同じ名前であっても衝突しません。
module ModFoo
struct Foo
x
end
function show(x)
print("!! " * string(x) * " !!")
end
end
Main.ModFoo
あるモジュール内の型や関数にアクセスするときは、<モジュール名>.<型/関数名>
というようにアクセスします。
f = ModFoo.Foo("something")
Main.ModFoo.Foo("something")
このとき、関数show
は元々用意されている関数show
とは衝突していません。
ModFoo.show(f)
!! Main.ModFoo.Foo("something") !!
show(f)
Main.ModFoo.Foo("something")
では、ModFoo
というモジュール名自体にはどのようにアクセスしているのでしょうか。
Juliaには、デフォルトのモジュールが既に用意されており、Main
と呼ばれています。
今まで書いてきた変数・関数・モジュールはすべてこのMain
モジュールに紐付けられており、ここを探索しているというわけです。
Main
Main
Main.quicksort([3,1,2])
3-element Array{Int64,1}: 1 2 3
Main.f
Main.ModFoo.Foo("something")
Main.ModFoo
Main.ModFoo
以下の様なモジュールGeo
を定義したとしましょう。
module Geo
export Point, distance, iswithin, move
struct Point
x::Float64
y::Float64
z::Float64
end
distance(a::Point, b::Point) = sqrt(sqdist(a, b))
iswithin(p::Point, c::Point, r::Real) = sqdist(p, c) <= r^2
function move(p::Point, dx, dy, dz)
return Point(p.x + dx, p.y + dy, p.z + dz)
end
sqdist(a::Point, b::Point) = (a.x - b.x)^2 + (a.y - b.y)^2 + (a.z - b.z)^2
end
Main.Geo
このモジュールで定義されている型や関数を、現在のモジュール(Main
)で使いたいときは、using
を使います。
using .Geo
using
を使うと、export
で指定された名前がGeo.
をつけなくても利用可能になります。
po = Point(0, 0, 0)
p1 = Point(1, 1, 1)
Point(1.0, 1.0, 1.0)
distance(po, p1)
1.7320508075688772
iswithin(p1, po, 1.7)
false
move(p1, -1, 0, 0)
Point(0.0, 1.0, 1.0)
iswithin(p1, po, 1.7)
false
しかし、使える関数は3行目のexport
で指定されたものだけです。sqdist
はexport
に指定されていないため使えません。
sqdist(po, p1)
UndefVarError: sqdist not defined Stacktrace: [1] top-level scope at In[207]:1
export
されていない場合は、モジュール名もつける必要があります。
Geo.sqdist(po, p1)
3.0
人の作ったモジュールを使うことで、開発の時間は短縮できます。
再利用を目的としたモジュールの集まりは、ひとつのパッケージにまとめられGitHubを中心に配布されています。
パッケージはMETADATA
というレポジトリに集められ、JuliaのREPLからPkg.add
関数を呼ぶだけで簡単にインストールできます。
パッケージを探すときは、http://pkg.julialang.org/を参照してみてください。
ここでは、Optim.jlパッケージを利用して、関数の数値最適化をしてみましょう。 まずはインストールです。
# 最初に実行するときだけ、下のコメントを消して実行して下さい
using Pkg
Pkg.update()
Pkg.add("Optim")
Updating registry at `~/.julia/registries/General` Updating git-repo `https://github.com/JuliaRegistries/General.git` Updating git-repo `git@github.com:bicycle1885/CellFishing.jl.git` Resolving package versions... Updating `~/.julia/environments/v0.7/Project.toml` [no changes] Updating `~/.julia/environments/v0.7/Manifest.toml` [no changes] Resolving package versions... Updating `~/.julia/environments/v0.7/Project.toml` [no changes] Updating `~/.julia/environments/v0.7/Manifest.toml` [no changes]
using Optim # インストール直後の読み込みはやや時間がかかる
Rosenbrock関数を最適化してみましょう。 この関数は以下のように定義され、 $(x, y) = (a, a^2)$ で最小値をとります。
$$f(x, y) = (a-x)^2 + b(y-x^2)^2$$func(x, a=1, b=100) = (a - x[1])^2 + b * (x[2] - x[1]^2)^2
func (generic function with 3 methods)
Optim.jlでは、optimize
関数をexport
しており、Nelder-Meadアルゴリズムを使って、実際の最小解 $(1, 1)$ をほぼ達成できています。
optimize(func, [10.0, 10.0])
Results of Optimization Algorithm * Algorithm: Nelder-Mead * Starting Point: [10.0,10.0] * Minimizer: [1.0000076575345525,1.0000090769441363] * Minimum: 3.950131e-09 * Iterations: 103 * Convergence: true * √(Σ(yᵢ-ȳ)²)/n < 1.0e-08: true * Reached Maximum Number of Iterations: false * Objective Calls: 198
versioninfo()
Julia Version 0.7.0 Commit a4cb80f3ed (2018-08-08 06:46 UTC) Platform Info: OS: macOS (x86_64-apple-darwin14.5.0) CPU: Intel(R) Core(TM) i5-6267U CPU @ 2.90GHz WORD_SIZE: 64 LIBM: libopenlibm LLVM: libLLVM-6.0.0 (ORCJIT, skylake) Environment: JULIA_SHELL = /bin/bash