Julia 言語の特徴

Takami Torao Julia 0.6.2 #julialang
  • このエントリーをはてなブックマークに追加

制御と構文構造

  • if は値を返す「式」である。
    julia> x = 0
    julia> y = if x <= 0; 100 else -100 end
    100
    julia> y = x <= 0? 100: -100
    100
  • for そのものは値を返さないが Python と同等の内包表記で配列を作成できる。
    julia> [i * 2 for i in 0:10 if i % 2 == 0]
    6-element Array{Int64,1}:
      0
      4
      8
     12
     16
     20

変数/関数と型システム

  • Java や Scala と同様に変数や関数などの識別子に Unicode 文字を使用できる。
    julia> Σ(x) = reduce(x) do a, b
             a + b
           end
    Σ (generic function with 1 method)
    
    julia> Σ([1, 2, 3])
    6
    REPL や Jupyter Notebook 上の Unicode 入力はタブ補間シーケンスを利用することができる。例えば fʼ を入力するには f\raps Tab の順で入力すれば良い。他に数学記号や Bold スタイル、合字、絵文字などに対応しており識別子を数式として見やすく表記できる。その他のシーケンスは Julia ドキュメントを参照。Skin Tone のような modifier symbols も可能。
  • 配列のリテラル表記はその要素から型を推測するが括弧の前に型を記述することで明示することができる。この方法で空の配列 [] に型を指定することができる。
    julia> x = [1, 2, 3.5]
    3-element Array{Float64,1}:
     1.0
     2.0
     3.5
    
    julia>  y = []
    0-element Array{Any,1}
    
    julia> z = Function[]
    0-element Array{Function,1}
  • 関数呼び出しの後方に do-end ブロックを付けた場合、ブロックは Function として関数の第一引数に挿入される。例えば以下の 2 つの呼び出しは同じ挙動。
    julia> f(g::Function, i, j) = g(i, j)
    julia> f((i, j) -> i + j, 1, 2)
    3
    julia> f(1, 2) do i, j
             i + j
           end
    3
  • 多重ディスパッチは同じ名前を持つ関数が実行時に渡されたパラメータの型によって適切な関数を選択し実行する。
    # A と B の型があり:
    struct A x::Int end
    struct B y::Int end
    
    # それぞれにディスパッチ可能な関数が自明であれば:
    to_json(a::A) = "{x:$(a.x)}"
    to_json(b::B) = "{y:$(b.y)}"
    
    # obj の型から対応する関数を動的に選択/実行することができる
    save(obj) = println(to_json(obj))
    
    save(A(2018))  # {x:2018} → OK
    save(B(0116))  # {y:116} → OK
    多重ディスパッチを使用する場所がスコープ外のモジュールの場合は新しく定義した型に対する関数をそのモジュールに追加する必要がある。
    module Framework
      export A, save
    
      # このモジュールでは型 A のみを定義
      struct A x::Int end
      to_json(a::A) = "{x:$(a.x)}"
    
      # Application モジュールから呼ばれる前にこのモジュールへ to_json(::B) の定義が追加されているは
      # ずなので B が渡されても多重ディスパッチで適切な関数を選択する
      save(obj) = println(to_json(obj))
    end
    
    module Application
      using Framework
    
      # このモジュールで新しい型 B を定義
      struct B y::Int end
      Framework.to_json(b::B) = "{y:$(b.y)}"
    
      save(A(2018))  # {x:2018} → OK
      save(B(0116))  # {y:116} → OK
    end
    外部からの定義を想定している関数はドキュメントとして指定するのが通例。

オブジェクト指向プログラミング

まずカプセル化、派生継承、多態化に相当する機能がないことから Julia はオブジェクト指向ではない。ただしかつての Ruby や Python もそうであったように後のバージョンで追加される余地はあるだろう。ここでは Julia での OO の考え方に類する手法を載せる。

  • クロージャーは束縛した外部のローカル変数を共有することができる。これをカプセル化に利用し、隠蔽された内部状態とそれを操作する公開されたメソッドとして扱うことができる。
    julia> function f()
               local x = 0
               ()->(x = x + 1; x), ()->(x = x + 2; x)
           end
    julia> g, h = f()
    julia> g()
    1
    
    julia> h()
    3
    
    julia> x
    ERROR: UndefVarError: x not defined
    
  • struct は C の構造体と同様にすべて公開されたフィールドであるためカプセル化の要件を満たすものではない。ただし意図せず状態を変更されたくないという目的であれば immutable struct (不変構造体) を利用することができる。
  • 多態化に相当する手法は多重ディスパッチを使用する。
  • 多重ディスパッチを使用せず、Module に定義された同名の関数を用いて多態化のように挙動を指定することもできる (まだ言語設計に OO を取り入れていなかった時代の Ruby や Python での OO 風手法と似ている)。
    module X
      f(i::Int) = i * 2
      g(i::Int) = i * i
    end
    
    module Y
      f(i::Float64) = i * 2
      g(i::Float64) = i * i
    end
    
    exec(value, operator::Module) = operator.f(value) + operator.g(value)
    
    exec(1, X)  # => 3
    exec(1.0, Y) # => 3.0
    関数を束ねる目的にモジュールを使っている違いこそあれ、関数型言語で高階関数を使って多態のような挙動をさせるのと意味的に同じ方法。多重ディスパッチと異なる点は、型に対して操作がかならずしも 1:1 である必要は無く同じ型に対して異なる挙動を指定することができる。また型に対する操作関数の定義先を一カ所に集約できる。
  • struct派生継承は不可能。また Ruby や Python のような Module の mix-in は include("filename.jl") のようにファイル単位の取り込みで代用するしかない。@mixin(module-name) のようなマクロが作成できるかは不明。

Julia においてオブジェクト指向に慣れた人向けに設計するのであれば mutable struct とそれを操作する module を実装することになるだろう。

関数型プログラミング

Julia は第一級関数をサポートしているものの今のところ末尾再帰最適化が実装されておらず、全体を式として書く関数型プログラミングのスタイルは取りづらい。

  • Julia は第一級関数をサポートしていて高階関数継続渡しといったスタイルで記述できる。
    julia> f(g::Function, x) = g(x) + g(x)
    julia> f(x -> x * x, 3)
    18
  • カリー化または部分適用はカリー化された関数を個別に定義する。
    julia> f(x, y) = 2y + x
    julia> fc(x) = y -> f(x, y)
    julia> fc(1)(2)
    5
  • 末尾再帰最適化 (tail call optimization) は clang 実装関数との互換運用で相性が悪いという理由で実装は見送られている。ループ実装の最適化が優先的に進めているようなので末尾再帰ではなくループを使用する。

非推奨

  • 可変な構造体を定義する際の type は非推奨で Julia 0.5 までの後方互換のため残されている。0.6 以降は mutable struct を使用する。