りんごがでている

何か役に立つことを書きます

X分で学ぶJulia

  • 2016/11/20 追記: この記事は内容が一部古くなっています。最新版はこちらを参照して下さい

Julia Advent Calendar 2014 1日目の記事です。

まだまだJuliaは一般に知られていない言語ということもありまして、Juliaの基本的な機能を最短で学ぶ記事を初日に書いた次第です。 X<30くらいだと思います。

バージョンはJuliaの最新リリース版であるv0.3系を基にしていますが、特に異なる点は次期バージョンであるv0.4に関しても触れています。

Juliaの公式サイトは The Julia Language で 、Juliaのバイナリは Julia Downloads から入手できます。

文法

百聞は一見にしかず。まずはJuliaのコードをざっと見てみましょう。

function mandel(z)
    c = z
    maxiter = 80
    for n = 1:maxiter
        if abs(z) > 2
            return n-1
        end
        z = z^2 + c
    end
    return maxiter
end

function randmatstat(t)
    n = 5
    v = zeros(t)
    w = zeros(t)
    for i = 1:t
        a = randn(n,n)
        b = randn(n,n)
        c = randn(n,n)
        d = randn(n,n)
        P = [a b c d]
        Q = [a b; c d]
        v[i] = trace((P.'*P)^4)
        w[i] = trace((Q.'*Q)^4)
    end
    std(v)/mean(v), std(w)/mean(w)
end

From: http://julialang.org/

どうでしょう?PythonRubyをやったことがる人なら初見でも大体の意味が分かるのではないでしょうか?

関数定義・分岐・反復などの構文はそれぞれfunction ... end, if ... end, for ... end, while ... endのようにキーワードで始まりendで終わります。 ちょうどRubyと同じような感じです。 インデントはPythonのように必要ではありませんが、4スペースでひとつのインデントを表すのが慣習です。

また、変数の宣言や型の指定は通常必要ありません。

こうしたコードはJuliaのLLVMベースのJITコンパイラによりコンパイルされ、C言語などで書いたコードとそれほど変わらない速度で実行できます。

変数

変数は特別に宣言せずとも初期化と同時に使用できます。

functionforなどほとんどのブロックは変数のスコープを新たに作ります。これはPythonなどと動作が異なりますので注意が必要です。例外的にスコープを作らないのはif ... endbegin ... endです。

for i in 1:10
    x = i
end
println(x)  # ERROR: x not defined
if true
    x = 10
end
println(x)  # OUTPUT: 10

トップレベルで定義された変数はグローバル変数になります。 グローバル変数コンパイラの最適化を難しくするため、constをつけて定義すべきです。

数値型

Juliaの値は型を持ちます。Juliaでは動的に型がつき、様々な機能と密接に関わってきます。

整数型はsigned, unsignedと8bit, 16bit, 32bit, 64bit, 128bitの組み合わせとBool型とBigInt型で合計12種類あり、 それぞれsigned 64bitはInt64やunsigned 32bitはUint32など一貫性のある型名がつけられています(v0.4でUintUIntに改名)。 浮動小数点の型も16bit, 32bit, 64bitとBigFloat型で合計4種類があります。 BigInt型とBigFloat型はそれぞれ任意精度の整数と浮動小数点数です。

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

科学計算のために作られているJuliaは、このように豊富な数値の方を持つ点が魅力のひとつです。

リテラル

大抵、何らかの値を作るリテラルは他の言語と同じです。

  • 数値
  • 文字(列)
    • 文字 Char型: 'a', '樹'
    • 文字列 ASCIIString型: "deadbeef", """Triple Quote String"""
  • その他
    • 真偽値 Bool型: true, false
    • シングルトン Nothing型: nothing (v0.4ではVoid型)

IntInt32またはInt64エイリアスです。

Juliaの対話実行環境(REPL)で確認してみましょう。 型はtypeof関数で確認できます。

~/s/JuliaAdvent $ julia
               _
   _       _ _(_)_     |  A fresh approach to technical computing
  (_)     | (_) (_)    |  Documentation: http://docs.julialang.org
   _ _   _| |_  __ _   |  Type "help()" for help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 0.3.0 (2014-08-20 20:43 UTC)
 _/ |\__'_|_|_|\__'_|  |
|__/                   |  x86_64-apple-darwin13.3.0

julia> 42
42

julia> typeof(42)
Int64

julia> typeof(1.0)
Float64

julia> '漢'
'漢'

julia> typeof('漢')
Char

julia> typeof("deadbeef")
ASCIIString (constructor with 2 methods)

julia> typeof(true)
Bool

julia> typeof(nothing)
Nothing (constructor with 1 method)

ベクトルは[]で記述できます。 Juliaではインデックスは1始まりなので注意が必要です。

julia> [1,2,3]
3-element Array{Int64,1}:
 1
 2
 3

julia> x = [1,2,3]
3-element Array{Int64,1}:
 1
 2
 3

julia> x[1]
1

以降対話セッションは>で表現します。

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になります。

(厳密には、Noneという具体型を含む全ての型のsubtypeになっている抽象型があります。)

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

> isleaftype(Int64)
true
> isleaftype(Int32)
true
> isleaftype(Integer)
false
> Int64 <: Integer
true
> Int32 <: Integer
true

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

type Person
    name::UTF8String
    age::Int
end

immutable Point
    x::Float64
    y::Float64
end

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

person = Person("佐藤建太", 24)
point = Point(1.0, 2.0)
println("Hello, my name is $(person.name) and $(person.age) years old.")

関数/メソッド

Juliaの関数定義には二種類の書き方があります。 手続き型言語っぽい通常の関数定義と、関数型言語っぽい=を用いた記法です。

# 手続き型風 (複数行のときに好まれる)
function add_one(x)
    x + 1
end

# 関数型風 (一行で書けるときに好まれる)
add_one(x) = x + 1

返り値を表すreturnPerlRubyのように省略可能です。

関数の引数は基本的に参照渡しのようになりますが、引数値の変数への束縛を変更できないため、immutableな型(例えばIntASCIIString)の値は関数内で変更できません。 逆にmutableな型(例えばArrayなどの配列)の値は関数内で変更することができます。

以下の例では標準のsort!関数で破壊的に配列の中身をソートしています。 Juliaでは破壊的な関数の名前に!をつけるのが慣習です。

> x = [4, 1, 2, 3]
4-element Array{Int64,1}:
 4
 1
 2
 3

> sort!(x)
4-element Array{Int64,1}:
 1
 2
 3
 4

> x
4-element Array{Int64,1}:
 1
 2
 3
 4

Juliaの他のスクリプト言語と違う大きな特徴のひとつが関数の 多重ディスパッチ(multiple dispatch) です。 これは、引数の型の組み合わせによって呼び出されるメソッドの実体が異なるという振る舞いを実現できます。

簡単な例で動作を確認しましょう。次の関数fooは4つの異なる実装を持ちます。

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(1)
foo Int: 1

> foo(1, 2)
foo Int Int: 1 2

> foo(1.0, 2.0)
foo Float64 Float64: 1.0 2.0

> foo(1, 2.0)
foo Int Float64: 1 2.0

> foo(1.0, 2)
ERROR: `foo` has no method matching foo(::Float64, ::Int64)

このように、Juliaではメソッドは関数に従属します。これは、メソッドがクラスに従属するクラスベースのプログラミング言語とは大きく異なります。

キーワード引数やオプショナル引数、可変長引数なども使えます。 =の後に値を書くとオプショナルな引数として認識され、引数の区切りを,の代わりに;を使うとそれ以降の引数がキーワード引数として扱われます。 args...とすると、仮引数argsは定義内ではタプルとなります。

# オプショナル引数
head(seq, n::Int=10) = seq[1:n]

# キーワード引数
function optimize(func; iter=100, rate=0.1, alg=GradientDescent)
    # ...
end

# 可変長引数
sumof(args...) = sum(args)

構文糖衣

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

係数

変数の前に数値をつけると、暗黙的に積だと解釈されます。 これは多項式の記述や12pxなど単位の記述に便利です。

> x = 2.1
2.1

> 2x
4.2

> 4x^2 + 3x + 2
25.94

範囲

配列から一部を取り出したり、ある範囲で反復すときなどに便利なのが範囲型です。 基本的な文法はstart:endもしくはstart:step:endのようにコロンで区切って記述します。 範囲自体もオブジェクトですので、変数に収めたり関数に渡したりもできます。

> 1:4
1:4

> for i in 1:4
    println(i)
  end
1
2
3
4

> 1:2:10
1:2:10

> for i in 1:2:10
    println(i)
  end
1
3
5
7
9

配列から一部を取り出すのにも範囲は使用されます。 終端はendで指定できます。

> x = [1,2,3,4,5]
5-element Array{Int64,1}:
 1
 2
 3
 4
 5

> x[1:3]
3-element Array{Int64,1}:
 1
 2
 3

> x[4:end]
2-element Array{Int64,1}:
 4
 5

> x[2:end-2]
2-element Array{Int64,1}:
 2
 3

内包表記

配列や辞書はPythonのような内包表記で記述できます。

> [x for x in 1:4]
4-element Array{Int64,1}:
 1
 2
 3
 4

> [c => i for (c, i) in zip('a':'f', 1:6)]
Dict{Char,Int64} with 6 entries:
  'd' => 4
  'f' => 6
  'b' => 2
  'e' => 5
  'c' => 3
  'a' => 1

ベクトル/行列

Juliaにおいてベクトルとは、一次元配列のことです。 ベクトルは以下のように記述します。

> [1, 2, 3]
3-element Array{Int64,1}:
 1
 2
 3

,を抜かすと、2次元の配列(行列)として解釈されます。

> [1 2 3]
1x3 Array{Int64,2}:
 1  2  3

複数行の行列の記述は、以下のように;で書けます。

> [1 2 3; 4 5 6]
2x3 Array{Int64,2}:
 1  2  3
 4  5  6

式の後に'をつけると行列の転置になります。

> [1 2 3; 4 5 6]'
3x2 Array{Int64,2}:
 1  4
 2  5
 3  6

無名関数

->を使うことで、無名関数を記述できます。

> x -> 2x
(anonymous function)

> map(x -> 2x, [1,2,3])
3-element Array{Int64,1}:
 2
 4
 6

ブロック引数

第一引数に関数をとる関数は、位置を逆転させて代わりにdo ... endブロックを取ることができます。 即ち、以下の2つが同じ意味になります。

# 関数引数が先
map(x -> 2x, [1,2,3])

# 関数引数の代わりにブロックをとる
map([1,2,3]) do x
    2x
end

これは、関数の中身が大きい場合に特に便利です。

さらに、ファイルを開いて自動的に閉じる処理の記述にも使われています。 以下のコードはファイルを開いて一行づつ処理をするパターンです。

open("somefile.txt") do f
    for line in eachline(f)
        # ...
    end
end

非標準文字列

Juliaでは文字列の前に識別子をおくと、通常の文字列ではなくマクロとして解釈されます。 これを利用して、正規表現を記述できます。

> 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`

> typeof(`ls -la`)
Cmd

> run(`ls -la`)
total 40
drwxr-xr-x+  4 kenta  staff    136 11 30 20:57 .
drwxr-xr-x+ 26 kenta  staff    884 11 29 17:17 ..
-rw-r--r--+  1 kenta  staff  12692 11 30 20:57 learnjulia.md
-rw-r--r--+  1 kenta  staff    234 11 29 18:10 sample.jl

便利なマクロ

@マークで始まる式は、コンパイル時に別の式に展開されます。この機能を マクロ といいます。 別の言い方をすれば、通常の関数が実行時に値を取って値を返すのに対し、マクロとはコンパイル時に式を取って式を返す関数と言えます。

以下の例はアサーションに失敗すると例外を投げる@assertマクロと、変数の状態をプリントする@showマクロです。 これら2つはデバッグなどに大変役立ちます。

> x = 100
100

> @assert x < 10
ERROR: assertion failed: x < 10
 in error at error.jl:21

> @show x
x = 100
100

@printfマクロは書式を指定したプリントのためのマクロです。

> @printf "line %3d: %s\n" 42 "foo"
line  42: foo

ベンチマークなどに使えるのが実行時間とアロケートされたメモリを測る@timeマクロです。 最初の呼び出し時にはJITコンパイル分のオーバーヘッドがかかりますので注意が必要です。

> fib(n) = if n < 2 1 else fib(n-1) + fib(n-2) end
fib (generic function with 1 method)

> @time fib(10)
elapsed time: 0.001739938 seconds (47552 bytes allocated)
89

> @time fib(20)
elapsed time: 9.9135e-5 seconds (96 bytes allocated)
10946

> @time fib(40)
elapsed time: 1.160873359 seconds (96 bytes allocated)
165580141

@code_llvm@code_nativeは関数呼び出しで実行されるLLVMとネイティブコードをプリントします。 パフォーマンスが上がらない時などに原因を探るのに便利です。

> @code_llvm fib(10)

define i64 @"julia_fib;42387"(i64) {
top:
  %1 = icmp sgt i64 %0, 1, !dbg !1280
  br i1 %1, label %L, label %if, !dbg !1280

if:                                               ; preds = %top
  ret i64 1, !dbg !1282

L:                                                ; preds = %top
  %2 = add i64 %0, -1, !dbg !1282
  %3 = call i64 @"julia_fib;42374"(i64 %2), !dbg !1282
  %4 = add i64 %0, -2, !dbg !1282
  %5 = call i64 @"julia_fib;42374"(i64 %4), !dbg !1282
  %6 = add i64 %5, %3, !dbg !1282
  ret i64 %6, !dbg !1282
}

> @code_native fib(10)
        .section        __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 1
        push    RBP
        mov     RBP, RSP
        push    R15
        push    R14
        push    RBX
        push    RAX
        mov     RBX, RDI
        cmp     RBX, 1
        jle     L67
Source line: 1
        lea     RDI, QWORD PTR [RBX - 1]
        movabs  R15, 4534292768
        call    R15
        mov     R14, RAX
        add     RBX, -2
        mov     RDI, RBX
        call    R15
        add     RAX, R14
L56:    add     RSP, 8
        pop     RBX
        pop     R14
        pop     R15
        pop     RBP
        ret
L67:    mov     EAX, 1
        jmpq    L56

マクロの書き方はちょっと入門の範囲を超えるので省略します。

パッケージ

JuliaのパッケージはGitレポジトリです。Julia本体もパッケージもGitHubを中心に回っています。 現在、Mercurialなど他のバージョン管理システムには対応していません。 しかし、パッケージ開発者を除くユーザとしてはGit自体を学ぶ必要はありません。 パッケージのインストールはJuliaの対話セッションから行うことができます。

> Pkg.add("DocOpt")
INFO: Installing DocOpt v0.0.2
INFO: Package database updated

ほか、レポジトリのメタデータのアップデートはPkg.update()で可能です。

公式に利用可能なパッケージはhttp://pkg.julialang.org/から一覧できます。 それ以外にもPkg.cloneを使うことで、公式レポジトリにはないがGitHubなどのGitレポジトリから直接インストールすることもできます。

その他

この記事では深く扱わなかった重要な部分については、Juliaのマニュアルへのリンクを張っておきます。 Juliaのマニュアルはとても読みやすく、それだけでプログラミング言語の勉強になるほど質の良いものです。