このチュートリアルはJuliaの機能を最速で学ぶためのものです。 Juliaは科学技術計算を得意とするプログラミング言語で、スクリプト言語のように手軽に書くことができるのにC言語やFortranにも負けない実行速度を誇ります。 文法や機能はPythonなど他のスクリプト言語とかなり共通しているので、別のプログラミング言語の知識があれば基本的な機能はすぐに習得できるでしょう。

他のスクリプト言語と比較した、Juliaの特異な点をいくつか挙げてみると、

  • コンパイル言語に比肩する実行速度
  • シンプルな言語機能と豊富な標準ライブラリ
  • 引数の型により実行される関数が決まる動的ディスパッチ
  • Lispのような強力なマクロ機能

というようなものが挙げられます。 それゆえ、今までのようにパフォーマンスが必要な部分をC言語など他の言語で書く必要はなく、すべてJuliaで書けます。 実際、文字列を含むJuliaの標準ライブラリはほとんどJuliaで書かれており、十分なパフォーマンスを持ちます。

ここで使用するJuliaのバージョンは2017年6月の最新版であるv0.6です。 それでは、早速Juliaを学んでいきましょう!

In [1]:
srand(0xdeadbeef);

Juliaの文法

まずはJuliaのコードをざっと見てみましょう。配列をソートするクイックソートのコードです。

In [2]:
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
Out[2]:
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言語などで書いたコードとそれほど変わらない速度で実行できます。

試しに整数の小さい配列をソートしてみると、うまく行っています。

In [3]:
quicksort([3, 6, 2, 4, 5, 1])
Out[3]:
6-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6

浮動小数点でも動きます。

In [4]:
quicksort([-2.1, 3.4, 5.0, -1.2, 3.1, 0.1])
Out[4]:
6-element Array{Float64,1}:
 -2.1
 -1.2
  0.1
  3.1
  3.4
  5.0

1千万要素もあるような浮動小数点数の配列のソートも一瞬です。

In [5]:
xs = randn(10_000_000)
@time quicksort(xs)
  1.329462 seconds (86 allocations: 76.300 MiB, 5.32% gc time)
Out[5]:
10000000-element Array{Float64,1}:
 -5.20182
 -5.05062
 -4.98326
 -4.97499
 -4.89386
 -4.86452
 -4.85004
 -4.81155
 -4.78594
 -4.75812
 -4.75195
 -4.75114
 -4.70064
  ⋮      
  4.64356
  4.66424
  4.68233
  4.68577
  4.69517
  4.71068
  4.82413
  4.8891 
  4.92069
  4.97683
  5.16999
  5.29364

ここでひとつ注目すべきことは、quicksort関数の定義時に引数の型を指定していなかったにも関わらず、整数にも浮動小数点数にも適用できるということです。 実は、関数の最初の実行時にそれぞれの型に応じて高速なコードを生成しています。 この機能のおかげで、Juliaでは関数の型を一切指定しなくても十分なパフォーマンスが得られます

quicksortは、数値にかぎらず以下の様な文字や文字列でも適用できます。

In [6]:
quicksort(['B', 'A', 'D', 'E', 'C'])
Out[6]:
5-element Array{Char,1}:
 'A'
 'B'
 'C'
 'D'
 'E'
In [7]:
quicksort(["Bob", "Alice", "Dave", "Eve", "Charlie"])
Out[7]:
5-element Array{String,1}:
 "Alice"  
 "Bob"    
 "Charlie"
 "Dave"   
 "Eve"    

変数

Juliaの変数名はとても自由です。英字やアンダースコア(_)から始まる英数字/アンダースコアの他に、UTF8の多様な文字が使えます。

使えるもの:

  • xyz
  • _foo3_
  • π
  • f′

使えないもの:

  • 33xyzなどの数値から始まるもの
  • for, function, endなど予約語
  • x.y, x:yなどの予約されている記号を使ったもの

xなどが使えるのはもちろんですが、

In [8]:
x = 100
Out[8]:
100

ηのようなギリシャ文字や漢字も使えます。

In [9]:
η = 0.01
Out[9]:
0.01
In [10]:
漢字変数 = η
Out[10]:
0.01

ここまで見てきたように、変数は特別に宣言せずとも初期化と同時に使用できます。

変数には、その影響するソースコードの範囲であるスコープという概念があります。 functionforなどで始まり、endで終わるほとんどのブロックは新たな変数のスコープを作ります。for ... endがスコープを作るのはPythonなどと動作が異なりますので注意が必要です。

以下の例では、変数xxfor ... endの内側のみのスコープを持つため、その外でxxにアクセスすると変数未定義の例外が発生しています。

In [11]:
for i in 1:10
    xx = i
end
xx
UndefVarError: xx not defined

例外的にスコープを作らないのはif ... endbegin ... endです。すなわち、if ... endbegin ... endの内側で定義した変数にはその外側でもアクセスできます。

In [12]:
if true
    yy = 10
end
yy
Out[12]:
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型はそれぞれ任意精度の整数と浮動小数点数です。

In [13]:
Int64
Out[13]:
Int64
In [14]:
Float64
Out[14]:
Float64

他には複素数のComplex{T}型があります。 Tというのは型パラメータ(type parameter)で、実部と虚部の数値の型を指定します。ちょうどC++のテンプレートやHaskellの型変数(type variable)のようなものです。

In [15]:
Complex{Float64}
Out[15]:
Complex{Float64}

特殊な例として、円周率のような定数はIrrational型として定義されています。

In [16]:
π
Out[16]:
π = 3.1415926535897...
In [17]:
isa(π, Irrational)  # isa(x, typ)は値xが型typの値であるかどうかを返す関数
Out[17]:
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
    • シングルトン Void型: nothing

実際のソースコードではInt型を目にすることが多いですが、これは環境によってInt32またはInt64のエイリアスになっています。

型は以下のようにtypeofで確認できます。

In [18]:
typeof(42)
Out[18]:
Int64
In [19]:
typeof(42.0)
Out[19]:
Float64
In [20]:
typeof(3 // 2)
Out[20]:
Rational{Int64}
In [21]:
typeof(2 + 3im)
Out[21]:
Complex{Int64}
In [22]:
typeof('A')
Out[22]:
Char
In [23]:
typeof('漢')
Out[23]:
Char
In [24]:
typeof("deadbeef")
Out[24]:
String
In [25]:
typeof("漢字")
Out[25]:
String
In [26]:
typeof(true)
Out[26]:
Bool
In [27]:
typeof(nothing)
Out[27]:
Void

配列 / タプル

Juliaでは1次元配列をベクトル(Vector)、2次元配列を行列(Matrix)とよびます。

ベクトル(1次元配列)は[x,y,...]で表現します。

In [28]:
[1,2,3]
Out[28]:
3-element Array{Int64,1}:
 1
 2
 3

添字のアクセスはカギ括弧([])を使います。添字は1から始まり、配列の長さ分で終わります。

In [29]:
# 5要素のベクトル
xs = [1,2,3,4,5]
Out[29]:
5-element Array{Int64,1}:
 1
 2
 3
 4
 5
In [30]:
xs[1]
Out[30]:
1
In [31]:
xs[5]
Out[31]:
5

値の更新も標準的な構文で可能です。

In [32]:
xs[2] = 200
Out[32]:
200
In [33]:
xs
Out[33]:
5-element Array{Int64,1}:
   1
 200
   3
   4
   5

配列を末尾からアクセスするときはendが使えます。

In [34]:
xs[end]
Out[34]:
5
In [35]:
xs[end] == xs[5]
Out[35]:
true
In [36]:
xs[end-1] == xs[4]
Out[36]:
true

ある範囲を切り出すにはn:mというような書き方ができます。

In [37]:
xs[2:3]
Out[37]:
2-element Array{Int64,1}:
 200
   3
In [38]:
xs[end-2:end]
Out[38]:
3-element Array{Int64,1}:
 3
 4
 5

型もちょっと見てみましょう。[x,y,...]で作られるのはArray型の値です。 以下の{Int64,1}の意味は後の型システムのところで説明します。

In [39]:
typeof([1,2,3])
Out[39]:
Array{Int64,1}

また、[1,2,3]Vector型の値でもあります。これはisa関数でチェックできます。

In [40]:
isa([1,2,3], Vector)
Out[40]:
true

行列は[a b c; d e f]のように書けます。

In [41]:
[1 2 3; 4 5 6]
Out[41]:
2×3 Array{Int64,2}:
 1  2  3
 4  5  6
In [42]:
x = [1 2 3;
     4 5 6]
Out[42]:
2×3 Array{Int64,2}:
 1  2  3
 4  5  6

添字でのアクセスも見ておきましょう。

In [43]:
x[1,2]
Out[43]:
2
In [44]:
x[2,end]
Out[44]:
6

Vectorの時と同様にMatrix型であることを確認しておきます。

In [45]:
isa([1 2 3; 4 5 6], Matrix)
Out[45]:
true

配列の要素数はlengthで取得します。

In [46]:
length([1,2,3])
Out[46]:
3
In [47]:
length([1 2 3; 4 5 6])
Out[47]:
6

タプル(組)は(x,y,...)です。

In [48]:
(1,2,3)
Out[48]:
(1, 2, 3)
In [49]:
typeof((1,2,3))
Out[49]:
Tuple{Int64,Int64,Int64}

タプルもベクトル同様、添字でのアクセスが出来ます

In [50]:
(1,2,3)[2]
Out[50]:
2
In [51]:
(1,2,3)[end]
Out[51]:
3

タプルの括弧は、曖昧性がなければ省略できます。

In [52]:
1,2,3
Out[52]:
(1, 2, 3)

配列の大きさはsize関数で得られますが、タプルとして返されます。

In [53]:
size([1,2,3])
Out[53]:
(3,)
In [54]:
size([1 2 3; 4 5 6])
Out[54]:
(2, 3)

タプルとベクトルはよく似ていますが、内部の構造や動作は大きく異なります。

まず、タプルは不変(immutable)ですが、ベクトルや配列は可変(mutable)です。 したがって、一度作ったタプルはそれ以降変更できませんが、配列では可能です。

また、タプルはメモリーの割当が起きないことがあるため、オブジェクトの生成コストが極めて小さいです。

範囲

Juliaには値の範囲を表す範囲型も用意されています。start:stopのように書くことで、startからstopまで、両端を含む範囲を表現します。

In [55]:
1:10
Out[55]:
1:10
In [56]:
'a':'z'
Out[56]:
'a':1:'z'

これはforループを書くときや、配列や文字列から一部分を切り出す際に用いられます。

In [57]:
for i in 3:6
    println(i)
end
3
4
5
6
In [58]:
x = [1,2,3,4,5,6]
Out[58]:
6-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
In [59]:
x[3:6]
Out[59]:
4-element Array{Int64,1}:
 3
 4
 5
 6

start:step:stopのように書くことで、ステップ幅を指定することもできます。

In [60]:
for i in 0:10:90
    println(i)
end
0
10
20
30
40
50
60
70
80
90

ステップ幅に-1を指定すれば、逆順の範囲も作れます。

In [61]:
for i in 5:-1:1
    println(i)
end
5
4
3
2
1

辞書

Juliaでは辞書ももちろん用意されています。

In [62]:
x = Dict("foo" => 1, "bar" => 2)
Out[62]:
Dict{String,Int64} with 2 entries:
  "bar" => 2
  "foo" => 1
In [63]:
x["foo"]
Out[63]:
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になります。

具体例で確認しましょう。Int64Int32は共に具体型で、Integerという抽象型のsubtypeになっています。 具体型はisleaftype、subtype/supertypeの関係は<:という関数で確認できます。

In [64]:
isleaftype(Int64)
Out[64]:
true
In [65]:
isleaftype(Int32)
Out[65]:
true
In [66]:
isleaftype(Integer)
Out[66]:
false
In [67]:
Int64 <: Integer
Out[67]:
true
In [68]:
Int32 <: Integer
Out[68]:
true

他には、Bool型などもInteger型のsubtypeです。

In [69]:
Bool <: Integer
Out[69]:
true

全ての型はsupertypeを一つだけ持ち、抽象型は複数のsubtypeを持つことができます。 それぞれsupertypesubtypesという関数があるので、少しみてみましょう。

In [70]:
supertype(Bool)
Out[70]:
Integer
In [71]:
subtypes(Integer)
Out[71]:
4-element Array{Union{DataType, UnionAll},1}:
 BigInt  
 Bool    
 Signed  
 Unsigned
In [72]:
subtypes(Signed)
Out[72]:
5-element Array{Union{DataType, UnionAll},1}:
 Int128
 Int16 
 Int32 
 Int64 
 Int8  

supertypeを辿って行くと、最終的にはAny型にたどり着きます。 次のようなtracetype関数で検証してみましょう。

In [73]:
function tracetype(t)
    print(t)
    while t != supertype(t)
        print(" <: ")
        t = supertype(t)
        print(t)
    end
    println()
end
Out[73]:
tracetype (generic function with 1 method)

どこから抽象型が共通になってるかなどにも注目して眺めてみて下さい。

In [74]:
tracetype(Int64)
Int64 <: Signed <: Integer <: Real <: Number <: Any
In [75]:
tracetype(Bool)
Bool <: Integer <: Real <: Number <: Any
In [76]:
tracetype(BigInt)
BigInt <: Integer <: Real <: Number <: Any
In [77]:
tracetype(Float64)
Float64 <: AbstractFloat <: Real <: Number <: Any
In [78]:
tracetype(String)
String <: AbstractString <: Any

数値の型に関しては、以下の図のようになっています。

型パラメータ

[1,2,3]は以下の様な型です。

In [79]:
typeof([1,2,3])
Out[79]:
Array{Int64,1}

これは、[1,2,3]Int64型を要素とする1次元の配列(ベクトル)という意味になります。一般化すると、Array{T,N}型はT型を要素とするN次元の配列です。 このように、Juliaでは型が別の型や値をパラメータとして持つことができます。

以下の例で、型パラメータに注目して確認してください。

In [80]:
[1.0, 2.0, 3.0]
Out[80]:
3-element Array{Float64,1}:
 1.0
 2.0
 3.0
In [81]:
typeof([1.0, 2.0, 3.0])
Out[81]:
Array{Float64,1}
In [82]:
[1 2 3]
Out[82]:
1×3 Array{Int64,2}:
 1  2  3
In [83]:
typeof([1 2 3])
Out[83]:
Array{Int64,2}
In [84]:
[1 2 3; 4 5 6]
Out[84]:
2×3 Array{Int64,2}:
 1  2  3
 4  5  6
In [85]:
typeof([1 2 3; 4 5 6])
Out[85]:
Array{Int64,2}

型定義

自分で型を作るのも簡単です。 以下のようにstructまたはmutable structに続けて型名を書き、フィールドを定義します。

In [86]:
mutable struct Person
    name::String
    age::Int
end

struct Location
    x::Float64
    y::Float64
end

デフォルトコンストラクタがありますので、即座にインスタンス化できます。 インスタンスのフィールドへはドット(.)でアクセスできます。

In [87]:
person = Person("ヤマダ田中", 34)
Out[87]:
Person("ヤマダ田中", 34)
In [88]:
person.name, person.age
Out[88]:
("ヤマダ田中", 34)

mutable structで作られた型のフィールド値は更新できます。

In [89]:
person.age += 1
Out[89]:
35
In [90]:
person
Out[90]:
Person("ヤマダ田中", 35)
In [91]:
loc = Location(1.0, 2.0)
Out[91]:
Location(1.0, 2.0)
In [92]:
loc.x, loc.y
Out[92]:
(1.0, 2.0)

structで作られた型のフィールド値は更新できません。

In [93]:
loc.x += 3.0
type Location is immutable

関数とメソッド

関数の定義

既に何度か出てきますが、関数の定義は

function <関数名>(<引数>, ...)
    <関数本体>
end

を使います。 返り値はreturnを使いますが、最後に評価された式が自動的に返り値になるので省略可能です。

In [94]:
# 2次元空間でのpとq間のユークリッド距離
function dist(p, q)
    dx = p[1] - q[1]
    dy = p[2] - q[2]
    sqrt((dx)^2 + (dy)^2)
end
Out[94]:
dist (generic function with 1 method)
In [95]:
dist((0, 0), (3, 4))
Out[95]:
5.0

もう一つ、別の方法として以下の「代入」のような形も使えます。一行で済むような 簡単な関数を定義するときに便利です。

In [96]:
dist(p, q) = sqrt((p[1] - q[1])^2 + (p[2] - q[2])^2)
Out[96]:
dist (generic function with 1 method)
In [97]:
dist((0, 0), (3, 4))
Out[97]:
5.0

オプション引数やキーワード引数、可変長引数もJuliaでは使えます。

In [98]:
# オプション引数: '='
ktop(xs, k=3) = sort(xs)[1:k]
Out[98]:
ktop (generic function with 2 methods)
In [99]:
ktop([1,5,3,2,6])
Out[99]:
3-element Array{Int64,1}:
 1
 2
 3
In [100]:
ktop([1,5,3,2,6], 2)
Out[100]:
2-element Array{Int64,1}:
 1
 2
In [101]:
# キーワード引数: ';' '='
function ktop(xs; k=3, rev=false)
    sort(xs, rev=rev)[1:k]
end
Out[101]:
ktop (generic function with 2 methods)
In [102]:
ktop([1,5,3,2,6], k=2)
Out[102]:
2-element Array{Int64,1}:
 1
 2
In [103]:
ktop([1,5,3,2,6], k=4, rev=true)
Out[103]:
4-element Array{Int64,1}:
 6
 5
 3
 2
In [104]:
function pathlength(p, q, rs...)
    len = dist(p, q)
    for r in rs
        len += dist(q, r)
        q = r
    end
    return len
end
Out[104]:
pathlength (generic function with 1 method)
In [105]:
pathlength((0, 0), (1, 1))
Out[105]:
1.4142135623730951
In [106]:
pathlength((0, 0), (1, 1), (1, 2))
Out[106]:
2.414213562373095
In [107]:
pathlength((0, 0), (1, 1), (1, 2), (0, 0))
Out[107]:
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では、関数がとる引数の型を<引数>::<型>のように指定することもできます。これは他のスクリプト言語にはあまりみられない特徴です。

In [108]:
function add(x::Int, y::Int)
    return x + y
end
Out[108]:
add (generic function with 1 method)

型が合えば関数を呼び出すことができます。

In [109]:
add(1, 2)
Out[109]:
3

しかし型が合わないと、呼び出すこともできません。

In [110]:
add(1.0, 2.0)
MethodError: no method matching add(::Float64, ::Float64)

ここで、次の関数を定義してみましょう

In [111]:
function add(x::Float64, y::Float64)
    return x + y
end
Out[111]:
add (generic function with 2 methods)

すると、先ほど呼び出せなかった方の組み合わせで呼び出せるようになります

In [112]:
add(1.0, 2.0)
Out[112]:
3.0

しかも、これは元の関数の上書きではありません。元のInt, Intの組み合わせでも呼び出せます。

In [113]:
add(1, 2)
Out[113]:
3

このように、引数の型に合わせて呼び出すメソッドを変える仕組みを多重ディスパッチと呼びます。 以下の例で、これがどのように動くのかを確認して下さい。

In [114]:
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
Out[114]:
foo (generic function with 4 methods)
In [115]:
foo(1)
foo Int: 1
In [116]:
foo(1, 2)
foo Int Int: 1 2
In [117]:
foo(1.0, 3.14)
foo Float64 Float64: 1.0 3.14
In [118]:
foo(1, 3.14)
foo Int Float64: 1 3.14

型の指定はInt64Float64のような具体型に限りません。 より高位の抽象型を使うこともできます。

In [119]:
add(BigInt(1), BigFloat(1))  # これにマッチするメソッドはまだない
MethodError: no method matching add(::BigInt, ::BigFloat)

typeof(x) <: Numberかつtypeof(x) <: Numberになれば、以下のメソッドを呼び出せますので、具体型を使っていたときに比べ適用範囲がぐっと広がります。

In [120]:
function add(x::Number, y::Number)
    return x + y
end
Out[120]:
add (generic function with 3 methods)
In [121]:
add(BigInt(1), BigFloat(1))  # 今度は呼び出せる
Out[121]:
2.000000000000000000000000000000000000000000000000000000000000000000000000000000
In [122]:
add(1, π), add(π, 1.0), add(true, false)  # 様々な型の組み合わせが使える
Out[122]:
(4.141592653589793, 4.141592653589793, 1)

コンストラクタ

Juliaのコンストラクタは通常の関数と同様に定義でき、多重ディスパッチも利用できます。 コンストラクタは以下の2種類に分けられます。

  • 内部コンストラクタ (inner constructor)
  • 外部コンストラクタ (outer constructor)

外部コンストラクタは、他の外部コンストラクタや内部コンストラクタを呼び出すことでオブジェクトを作ることができます。 最終的に、全てのオブジェクトは内部コンストラクタを経由して作られるため、内部コンストラクタで最終的な値のチェックなどを実現できます。

それでは、具体例を見てみましょう。

以下のRGB型はRed-Green-Blueの3原色を指定して色を表現する型です。 それぞれの色は8bitの符号なし整数でエンコーディングしています。 しかし、色を作る際は8bitで表現できない値が与えられる可能性があるため、不正な値が与えられれば例外を投げるようにしたいです。 これを実現するため、内部コンストラクタで与えられた値のチェックをしています。 内部コンストラクタは、構文的にはtype ... endの内部で定義された関数です。 内部コンストラクタの特別な点は、new関数を呼び出すことで、その型のオブジェクトを作ることができるところにあります。

In [123]:
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
In [124]:
RGB(1, 2, 3)
Out[124]:
RGB(0x01, 0x02, 0x03)

300など8bitで表現できない色が与えられたら、@assertマクロが例外を投げます。

In [125]:
RGB(2, 300, 2)
AssertionError: 0 <= g <= 255

Stacktrace:
 [1] RGB(::Int64, ::Int64, ::Int64) at ./In[123]:8

デフォルトコンストラクタはnew関数を呼び出すだけの内部コンストラクタで、プログラマが明示的に内部コンストラクタを定義するとデフォルトコンストラクタは作られません。

ある1つの値を与えたら、R,G,Bすべてに同じ値を設定する簡便なコンストラクタが欲しいかもしれません。 その場合は以下のように外部コンストラクタを使うと便利です。

In [126]:
RGB(x) = RGB(x, x, x)
Out[126]:
RGB

外部コンストラクタは構文的にはtype ... endの外部で定義された関数です。ここではnew関数は使えず、内部コンストラクタや他の外部コンストラクタを呼び出すことでオブジェクトを作ります。

これは、他の外部コンストラクタを呼び出す外部コンストラクタの例です。

In [127]:
RGB() = RGB(0)
Out[127]:
RGB

多重ディスパッチのお陰で、以下の3つのコンストラクタはすべて使うことができます。

In [128]:
RGB(), RGB(10), RGB(10, 20, 30)
Out[128]:
(RGB(0x00, 0x00, 0x00), RGB(0x0a, 0x0a, 0x0a), RGB(0x0a, 0x14, 0x1e))

構文糖衣

ここではJuliaで見られる特徴的な構文をざっと見て行きます。

係数

変数や関数の前に数値を置くことで、その値との積を表現できます。

In [129]:
x = 2.1
Out[129]:
2.1
In [130]:
2x
Out[130]:
4.2
In [131]:
4x^2 + 3x - 2
Out[131]:
21.94
In [132]:
x = linspace(0, 1);
In [133]:
4sin.(2x) - 3cos.(4x)
Out[133]:
50-element Array{Float64,1}:
 -3.0     
 -2.82679 
 -2.63394 
 -2.42191 
 -2.19132 
 -1.94288 
 -1.67746 
 -1.39602 
 -1.09965 
 -0.789545
 -0.467011
 -0.133436
  0.209699
  ⋮       
  6.99687 
  6.99646 
  6.96942 
  6.91591 
  6.8362  
  6.73074 
  6.60009 
  6.44496 
  6.26617 
  6.0647  
  5.84162 
  5.59812 

内包表記

[]内にforループを書き、ベクトルや行列などの配列を作ることができます。

In [134]:
[x for x in 1:4]
Out[134]:
4-element Array{Int64,1}:
 1
 2
 3
 4
In [135]:
[x * y for x in 1:4, y in 1:5]
Out[135]:
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をつけることでフィルターをかけることもできます。

In [136]:
[x for x in 1:10 if isodd(x)]
Out[136]:
5-element Array{Int64,1}:
 1
 3
 5
 7
 9

辞書も可能です。

In [137]:
Dict(c => i for (i, c) in enumerate('a':'z'))
Out[137]:
Dict{Char,Int64} with 26 entries:
  'g' => 7
  'a' => 1
  'd' => 4
  'l' => 12
  'm' => 13
  'p' => 16
  'q' => 17
  'b' => 2
  't' => 20
  'e' => 5
  's' => 19
  'v' => 22
  'n' => 14
  'f' => 6
  'i' => 9
  'u' => 21
  'y' => 25
  'z' => 26
  'x' => 24
  'r' => 18
  'c' => 3
  'o' => 15
  'w' => 23
  'j' => 10
  'h' => 8
  ⋮   => ⋮

転置

行列を転置するにはシングルクォート(')を末尾につけるだけです。

In [138]:
x = [1 2 3; 4 5 6]
Out[138]:
2×3 Array{Int64,2}:
 1  2  3
 4  5  6
In [139]:
x'
Out[139]:
3×2 Array{Int64,2}:
 1  4
 2  5
 3  6

transpose関数でも転置できます。

In [140]:
transpose(x)
Out[140]:
3×2 Array{Int64,2}:
 1  4
 2  5
 3  6

無名関数

一時的に使う関数などを、名前を付けずに作ることができます。

In [141]:
x -> 2x
Out[141]:
(::#10) (generic function with 1 method)
In [142]:
map(x -> 2x, [1,2,3])
Out[142]:
3-element Array{Int64,1}:
 2
 4
 6

ブロック引数

第一引数に関数を取れる関数では、do ... endを使うことで無名関数を後置することもできます。

In [143]:
map([1,2,3]) do x
    2x
end
Out[143]:
3-element Array{Int64,1}:
 2
 4
 6

この機能は、ファイルを開いて処理をし、終わったら自動的にファイルを閉じるといった場面で多用されます。

In [144]:
open("sample.txt") do io
    for (n, line) in enumerate(eachline(io))
        print("$n: $line")
    end
end
1: one2: two3: three

非標準文字列 (non-standard string literals)

"..."のような文字列リテラルの前に識別子を起き、その文字列を元に様々な操作を行うことができます。 以下は、r"..."で正規表現オブジェクトを作る例です。

In [145]:
r"\w-\d \w+"
Out[145]:
r"\w-\d \w+"
In [146]:
match(r"\w-\d \w+", "B-2 Spirit")
Out[146]:
RegexMatch("B-2 Spirit")

バージョン番号を作るのにも使われます。

In [147]:
v"1.2.3"
Out[147]:
v"1.2.3"

外部コマンド

バッククウォートを使って外部コマンドを作成し、run関数で実行することができます。

In [148]:
`ls -la`
Out[148]:
`ls -la`
In [149]:
run(`ls -la`)
total 536
drwxr-xr-x  11 kenta  staff     374 Jun  8 00:05 .
drwxr-xr-x  85 kenta  staff    2890 Sep 18  2016 ..
-rw-r--r--@  1 kenta  staff    6148 Dec 21  2015 .DS_Store
drwxr-xr-x  14 kenta  staff     476 Jun  7 23:55 .git
-rw-r--r--   1 kenta  staff      20 Dec 21  2015 .gitignore
drwxr-xr-x   5 kenta  staff     170 Jun  8 00:01 .ipynb_checkpoints
-rw-r--r--   1 kenta  staff  125092 Jun  8 00:05 Julia高速チュートリアル-0.6.ipynb
-rw-r--r--   1 kenta  staff  125316 Jun  7 23:58 Julia高速チュートリアル.ipynb
-rw-r--r--   1 kenta  staff     553 Oct 24  2016 README.md
drwxr-xr-x   4 kenta  staff     136 Dec 21  2015 images
-rw-r--r--   1 kenta  staff      14 Apr 29  2015 sample.txt
In [150]:
run(`cat -n sample.txt`)
     1	one
     2	two
     3	three

メタプログラミング

Juliaには、Juliaのプログラムを使ってJuliaのコードを書き換えることができるメタプログラミングという機能があります。JuliaではLisp言語の影響から、この機能をマクロとも呼んでいます。 これを使うと、書くコードの量を劇的に減らしたり、普通の関数では実現できないような特異な働きをさせることができます。

Lispなどでは一般的に使われるマクロですが、他の言語にはあまり見られず、あったとしても自分で定義して使うことは稀でしょう。このチュートリアルでは既に定義されているマクロを使用してみて、マクロではどのようなことが可能になるのかを見ることにします。

@show

@showマクロは、変数の値を確認する際に大変便利なマクロです。 これを使うと、コードのある部分で変数が何だったのかを瞬時に知ることができます。

In [151]:
x = 3.14
Out[151]:
3.14
In [152]:
@show x
x = 3.14
Out[152]:
3.14

挿入ソート(insertion sort)の動作を確認するために、@showマクロを使ってみましょう。

In [153]:
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
Out[153]:
insertionsort! (generic function with 1 method)

これを実行してみると、ベクトルが左から順にソートされていく様子がよくわかるのではないでしょうか。

In [154]:
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]
Out[154]:
8-element Array{Int64,1}:
 1
 2
 3
 4
 5
 6
 7
 8

@assert

@assertマクロは、簡便な不定条件のチェックに便利なマクロです。

In [155]:
x = 1
Out[155]:
1

条件が満たされるときは、何もしませんが、

In [156]:
@assert x == 1

条件に違反すると、例外を投げます。

In [157]:
@assert x == 2
AssertionError: x == 2

エラーメッセージは自分で書くこともできます。

In [158]:
@assert x == 2 "xが$(x)ですよ! xは2じゃないとダメです!"
AssertionError: xが1ですよ! xは2じゃないとダメです!

@time

@timeマクロは、関数やコード片の実行時間とメモリ使用量を測るのに便利なマクロです。

先ほど定義したinsertionsort@showを消して、quicksortとパフォーマンスを比較してみましょう。

In [159]:
insertionsort(xs) = insertionsort!(copy(xs))

function insertionsort!(xs)
    [email protected] 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
        [email protected] xs
    end
    xs
end

insertionsort([1.0, 0.0])
Out[159]:
2-element Array{Float64,1}:
 0.0
 1.0

小さい配列だとinsertionsortquicksortが同程度に速いようですが、

In [160]:
xs = randn(10);
In [161]:
gc()
@time for _ in 1:100000; insertionsort(xs); end
  0.028307 seconds (100.02 k allocations: 15.260 MiB)
In [162]:
gc()
@time for _ in 1:100000; quicksort(xs); end
  0.021378 seconds (100.00 k allocations: 15.259 MiB)

大きい配列ではquicksortの方が断然高速です。

In [163]:
xs = randn(10000);
In [164]:
gc()
@time insertionsort(xs);
  0.048746 seconds (6 allocations: 78.359 KiB)
In [165]:
gc()
@time quicksort(xs);
  0.000900 seconds (6 allocations: 78.359 KiB)

数値計算

Juliaを使う人の多くは、数値計算を目的としていると思います。 ここで、標準ライブラリで使える便利な関数を紹介していきます。

配列の確保

[1,2,3]のようにリテラルで直接配列を確保する以外にも、配列を確保する便利な関数が多数あります。

0で埋められた配列が欲しい場合は、zerosを使います。 基本的には、zeros(<型>, <サイズ>)という書き方をします。

In [166]:
x = zeros(Float64, 4)
Out[166]:
4-element Array{Float64,1}:
 0.0
 0.0
 0.0
 0.0
In [167]:
x = zeros(Float64, (4, 3))
Out[167]:
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
In [168]:
x = zeros(Float64, (4, 3, 2))
Out[168]:
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です。

In [169]:
ones(Float64, 4)
Out[169]:
4-element Array{Float64,1}:
 1.0
 1.0
 1.0
 1.0

単位行列はeyeで作れます。

In [170]:
eye(Float64, 3)
Out[170]:
3×3 Array{Float64,2}:
 1.0  0.0  0.0
 0.0  1.0  0.0
 0.0  0.0  1.0

もうちょっと複雑な初期化をしたい場合は、内包表記を使うのが便利でしょう。

In [171]:
[Float64(i > j) for i in 1:4, j in 1:3]
Out[171]:
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のコンストラクタを呼ぶこともできます。

In [172]:
Array{Float64}((3, 4))
Out[172]:
3×4 Array{Float64,2}:
 2.40852e-314  2.40852e-314  2.40852e-314  2.40852e-314
 2.40852e-314  2.40852e-314  2.40852e-314  2.40853e-314
 2.40852e-314  2.40852e-314  2.40852e-314  2.40862e-314

Float64型はよく使うので、省略可能です。

In [173]:
ones((3, 4))
Out[173]:
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

線形代数

ベクトルや配列の和や積は、数値の時のように行えます。

In [174]:
x = [1.0, 2.0, 3.0]
Out[174]:
3-element Array{Float64,1}:
 1.0
 2.0
 3.0
In [175]:
x + 1.0
Out[175]:
3-element Array{Float64,1}:
 2.0
 3.0
 4.0
In [176]:
x - 1.0
Out[176]:
3-element Array{Float64,1}:
 0.0
 1.0
 2.0
In [177]:
x * 5
Out[177]:
3-element Array{Float64,1}:
  5.0
 10.0
 15.0
In [178]:
5x
Out[178]:
3-element Array{Float64,1}:
  5.0
 10.0
 15.0
In [179]:
x / 2
Out[179]:
3-element Array{Float64,1}:
 0.5
 1.0
 1.5

ベクトルどうしの和も計算できます。

In [180]:
x + 4x
Out[180]:
3-element Array{Float64,1}:
  5.0
 10.0
 15.0
In [181]:
0.3 * (x - 1) + 0.7 * (x + 1)
Out[181]:
3-element Array{Float64,1}:
 1.4
 2.4
 3.4

ひとつ気をつけなければいけないことは、ベクトルの要素ごとの積(アダマール積)は、.*を使わなければならないということです。これは、*がベクトルや行列自体の積に使われているためです。

In [182]:
x * 2x
DimensionMismatch("Cannot multiply two vectors")

Stacktrace:
 [1] *(::Array{Float64,1}, ::Array{Float64,1}) at ./linalg/rowvector.jl:184
In [183]:
x .* 2x
Out[183]:
3-element Array{Float64,1}:
  2.0
  8.0
 18.0

要素ごとの除算も同様です。

In [184]:
2x ./ x
Out[184]:
3-element Array{Float64,1}:
 2.0
 2.0
 2.0

内積

In [185]:
dot(x, x)
Out[185]:
14.0

行列とベクトルの積

In [186]:
A = [Float64(i + j) for i in 1:2, j in 1:3]
Out[186]:
2×3 Array{Float64,2}:
 2.0  3.0  4.0
 3.0  4.0  5.0
In [187]:
A * x
Out[187]:
2-element Array{Float64,1}:
 20.0
 26.0

行列の積

In [188]:
A' * A
Out[188]:
3×3 Array{Float64,2}:
 13.0  18.0  23.0
 18.0  25.0  32.0
 23.0  32.0  41.0

行列の積は次のように*を省略しても書けます。

In [189]:
A'A
Out[189]:
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$

In [190]:
b = [20.0, 26.0]
A \ b
Out[190]:
3-element Array{Float64,1}:
 1.0
 2.0
 3.0

モジュール

すべての名前が一つの場所にあると、関数名などが衝突して予期せぬ動作をすることがあります。 特に、Juliaのように多重ディスパッチがあるときは問題が複雑になりかねません。 そういった問題を回避するためにも、名前空間を分けるJuliaのモジュールシステムを理解することは重要です。

Juliaでは、モジュールはmodule ... endで明示的に定義します。 moduleendで囲まれた部分のコードは、他の名前空間とは別の名前空間になります。 つまり、型や関数名は他のモジュールと同じ名前であっても衝突しません。

In [191]:
module ModFoo

struct Foo
    x
end

function show(x)
    print("!! " * string(x) * " !!")
end

end
Out[191]:
ModFoo

あるモジュール内の型や関数にアクセスするときは、<モジュール名>.<型/関数名>というようにアクセスします。

In [192]:
f = ModFoo.Foo("something")
Out[192]:
ModFoo.Foo("something")

このとき、関数showは元々用意されている関数showとは衝突していません。

In [193]:
ModFoo.show(f)
!! ModFoo.Foo("something") !!
In [194]:
show(f)
ModFoo.Foo("something")

では、ModFooというモジュール名自体にはどのようにアクセスしているのでしょうか。 Juliaには、デフォルトのモジュールが既に用意されており、Mainと呼ばれています。 今まで書いてきた変数・関数・モジュールはすべてこのMainモジュールに紐付けられており、ここを探索しているというわけです。

In [195]:
Main
Out[195]:
Main
In [196]:
Main.quicksort([3,1,2])
Out[196]:
3-element Array{Int64,1}:
 1
 2
 3
In [197]:
Main.f
Out[197]:
ModFoo.Foo("something")
In [198]:
Main.ModFoo
Out[198]:
ModFoo

モジュールの使い方

以下の様なモジュールGeoを定義したとしましょう。

In [199]:
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
Out[199]:
Geo

このモジュールで定義されている型や関数を、現在のモジュール(Main)で使いたいときは、usingを使います。

In [200]:
using Geo

usingを使うと、exportで指定された名前がGeo.をつけなくても利用可能になります。

In [201]:
po = Point(0, 0, 0)
p1 = Point(1, 1, 1)
Out[201]:
Geo.Point(1.0, 1.0, 1.0)
In [202]:
distance(po, p1)
Out[202]:
1.7320508075688772
In [203]:
iswithin(p1, po, 1.7)
Out[203]:
false
In [204]:
move(p1, -1, 0, 0)
UndefVarError: move not defined
In [205]:
iswithin(p1, po, 1.7)
Out[205]:
false

しかし、使える関数は3行目のexportで指定されたものだけです。sqdistexportに指定されていないため使えません。

In [206]:
sqdist(po, p1)
UndefVarError: sqdist not defined

exportされていない場合は、モジュール名もつける必要があります。

In [207]:
Geo.sqdist(po, p1)
Out[207]:
3.0

パッケージ

人の作ったモジュールを使うことで、開発の時間は短縮できます。 再利用を目的としたモジュールの集まりは、ひとつのパッケージにまとめられGitHubを中心に配布されています。 パッケージはMETADATAというレポジトリに集められ、JuliaのREPLからPkg.add関数を呼ぶだけで簡単にインストールできます。

パッケージを探すときは、http://pkg.julialang.org/を参照してみてください。

ここでは、Optim.jlパッケージを利用して、関数の数値最適化をしてみましょう。 まずはインストールです。

In [208]:
# 最初に実行するときだけ、下のコメントを消して実行して下さい
#Pkg.update()
#Pkg.add("Optim")
In [209]:
using Optim

Rosenbrock関数を最適化してみましょう。 この関数は以下のように定義され、 $(x, y) = (a, a^2)$ で最小値をとります。

$$f(x, y) = (a-x)^2 + b(y-x^2)^2$$
In [210]:
func(x, a=1, b=100) = (a - x[1])^2 + b * (x[2] - x[1]^2)^2
Out[210]:
func (generic function with 3 methods)

Optim.jlでは、optimize関数をexportしており、Nelder-Meadアルゴリズムを使って、実際の最小解 $(1, 1)$ をほぼ達成できています。

In [211]:
optimize(func, [10.0, 10.0])
Out[211]:
Results of Optimization Algorithm
 * Algorithm: Nelder-Mead
 * Starting Point: [10.0,10.0]
 * Minimizer: [1.000046451299345,1.0000921950079538]
 * Minimum: 2.208097e-09
 * Iterations: 110
 * Convergence: true
   *  √(Σ(yᵢ-ȳ)²)/n < 1.0e-08: true
   * Reached Maximum Number of Iterations: false
 * Objective Function Calls: 153