メタプログラミング

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

導入

Julia はコードを AST (abstract syntax tree; 抽象構文木) として評価する機能を持っている。ここでの評価とはコード上に AST を展開し実行することを意味する。このようなコード展開による評価は、強固なコンテキスト分離を持つ関数の呼び出しなどと異なり、実行コンテキストに強く影響する (黒魔術的な) 機能を実装することができる。

マクロはこのメタプログラミング機能を利用して評価時 (コンパイル時) に新しいコードを生成したり、解釈済みの AST を変形する機能である。マクロ機能は AOT (aspect oriented programming)、注釈 (annotation)、DSL (domain specific language)、テンプレートといった用途に使用することができる。

AST の作成

Julia 標準の parse() 関数は与えられた文字列をそのまま Julia のコードとして解釈し Expr (AST) を生成する。この Expr は eval() で評価 (実行) することができる。

julia> expr = parse("2 * (1 + 1)")
:(2 * (1 + 1))

julia> typeof(expr)
Expr

julia> eval(expr)
4

上記の挙動は JavaScript や Ruby, Python といった動的型付き言語での eval() と似ているが、文字列を直接コードとして実行するのではなく parse() で解釈した AST を評価する点が異なる。

Expr は文字列を解釈して生成する以外にも :(…) を使用して通常のコードとして記述することができる。

julia> expr = :(2 * (1 + 1))
:(2 * (1 + 1))

複数行の文で記述する場合は quote~end で囲う。以下のコードにおいて local 宣言した変数は eval() で評価した実行コンテキストには影響しないことに注意。

julia> expr = quote
         local x = 1
         local y = 2
         x + y
       end
quote  # REPL[7], line 2:
    local x = 1 # REPL[7], line 3:
    local y = 2 # REPL[7], line 4:
    x + y
end

julia> eval(expr)
3

julia> y
ERROR: UndefVarError: y not defined

変数

Julia のメタプログラミングは eval() を実行した場所に AST を展開/挿入することと等価である。このような実行コンテキストへの強い依存でプログラマが意図した結果をもたらすために AST の変数は以下の 3 つの挙動がある。

  1. AST に閉じたローカル変数。前述のように local 付きで宣言することで AST 評価時の実行コンテキスト (Expr の外の同名の変数) に影響を与えない変数として使用することができる。
    julia> expr = :(local x = 100; x)
    julia> x = 20
    julia> eval(expr)
    100
    julia> x
    20
  2. AST が展開されたときの実行コンテキストの変数値。変数に何も付けないか global と明記した場合に適用される。変数値の参照や変更は AST の外に影響する。
    julia> expr = :(global x = 200; x)
    julia> x = 20
    julia> eval(expr)
    200
    julia> x
    200
  3. AST が作成されたときの実行コンテキストの変数値。文字列の補間 (interpolation) と同様に変数の先頭に $ を付ける (式であれば $(...) で囲う) ことで宣言時の変数の評価結果が埋め込まれる。
    julia> x = 80
    julia> expr = :($x)
    julia> x = 50
    julia> eval(expr)
    80

以下の例では $ や local を付けていない x が評価時のコンテキストでの x を参照しているのに対して、補間として Expr に埋め込まれた $pi の値はコンテキストに影響を受けていないことが分かる。

julia> expr = :(x * $pi)
:(x * π = 3.1415926535897...)

julia> eval(expr)
ERROR: UndefVarError: x not defined
Stacktrace:
 [1] eval(::Module, ::Any) at .\boot.jl:235
 [2] eval(::Any) at .\boot.jl:234

julia> x = 2
2

julia> eval(expr)
6.283185307179586

julia> pi = 10
WARNING: imported binding for pi overwritten in module Main
10

julia> eval(expr)
6.283185307179586

global の挙動は immutable な言語制約に慣れている開発者には危険に思えるだろう。実際 $ や local の付け忘れをテストで検出することは難しく、利用者コードの変数の参照、変更、または関数のすり替えは意図しないセキュリティ脆弱性の原因になるだろう。

開発者は変数が宣言時に展開されるか評価時に展開されるかを十分に注意し、可能な限り local と $ 付きの変数のみで使用すべきである。

関数

AST では関数も呼び出すことができる。ただし AST 生成時点に参照できる関数にバインドされるわけではなく、あくまで実行コンテキストのシンボルに。ではない。$ による補間は 1)関数名のシンボルが埋め込まれる 2)関数の実行結果が埋め込まれる

julia> foo() = 1 + 1
foo (generic function with 1 method)

julia> bar() = 2 + 2
bar (generic function with 1 method)

julia> expr = :(foo() + $bar())
:(foo() + (bar)())

julia> eval(expr)
6

julia> foo() = 3 + 3
foo (generic function with 1 method)

julia> bar() = 4 + 4
bar (generic function with 1 method)

julia> eval(expr)
14

注意しなければならないのは、意図しない変更が行われ非常にわかりにくいバグを埋め込むことになる。

julia> module A
         foo() = 1
         export foo
       end

julia>  module B
         using A
         macro bar()
           :(foo() + $foo())
         end
         export @bar
       end

julia> using B

julia> @macroexpand @bar
:((B.foo)() + (A.foo)())

julia> @bar
2

マクロ

Julia のマクロ機能を使用してコンパイル時に AST を操作することができる。

参照

  1. Julia ASTs