メタプログラミング
Julia に残る最も大きな Lisp のレガシーがメタプログラミングのサポートです。Lisp と同様、Julia は自身のコードを自身の言語のデータ構造で表現します。Julia 言語のコードは同じ言語から作成・操作できるので、コードを変形・作成するプログラムを書くことができます。メタプログラミングにより追加のビルドステップを必要としない洗練されたコード生成、そして抽象構文木 (AST) のレベルで処理を行う真の Lisp スタイルマクロが可能になります。これに対して、C や C++ のようなプリプロセッサによる "マクロ" システムは文字的な操作と置換をコードのパースと解釈の前に行うだけです。Julia ではデータ型とコードが全て Julia のデータ構造として表現されるので、強力なリフレクションの機能を利用してプログラムの内部構造や型を他のデータと同様に詳しく調べることができます。
プログラムの表現
任意の Julia プログラムは最初文字列として表されます:
julia> prog = "1 + 1"
"1 + 1"
最初に起こるのは?
最初のステップはパースと呼ばれる処理であり、文字列が式 (expression) と呼ばれるオブジェクトに変換されます。Julia において式は Expr
型のオブジェクトで表されます:
julia> ex1 = Meta.parse(prog)
:(1 + 1)
julia> typeof(ex1)
Expr
Expr
オブジェクトは二つの部分からなります:
-
式の種類を識別するシンボル (
Symbol
型の値)head
: シンボルは識別子を表すインターン化文字列です (詳しくは後述します):julia> ex1.head :call
-
式の引数
args
: シンボル・別の式・リテラル値のいずれかからなる配列です:julia> ex1.args 3-element Array{Any,1}: :+ 1 1
式は前置記法を使った直接的な構築も可能です:
julia> ex2 = Expr(:call, :+, 1, 1)
:(1 + 1)
パースによって得られる式と直接構築した式は等価です:
julia> ex1 == ex2
true
ここで重要なのは Julia コードの内部表現が Julia からアクセスできるデータ構造であることです。
dump
関数を使うと、Expr
オブジェクトを注釈とインデントを付けた状態で出力できます:
julia> dump(ex2)
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 1
Expr
オブジェクトは入れ子にできます:
julia> ex3 = Meta.parse("(4 + 4) / 2")
:((4 + 4) / 2)
式は Meta.show_sexpr
を使っても出力できます。この関数が出力するのは与えられた式 Expr
の S-式 であり、Lisp ユーザーであれば見慣れているでしょう。入れ子になった Expr
の S-式の例を示します:
julia> Meta.show_sexpr(ex3)
(:call, :/, (:call, :+, 4, 4), 2)
シンボル
Julia の構文において文字 :
には二つの用途があります。一つ目の用途はシンボル (Symbol
型の値) の作成です。シンボルはインターン化文字列であり、式を構成する基礎要素の一つとして使われます:
julia> :foo
:foo
julia> typeof(ans)
Symbol
Symbol
のコンストラクタは任意個の引数を受け取り、それらの文字列表現を連結した新しいシンボルを作成します:
julia> :foo == Symbol("foo")
true
julia> Symbol("func",10)
:func10
julia> Symbol(:var,'_',"sym")
:var_sym
:
構文を使った場合にはシンボルの名前が識別子として正当である必要があることに注意してください。識別子として正当でない文字列のシンボルは Symbol(str)
で構築する必要があります。
式に含まれるシンボルは変数へのアクセスを表します: 式が評価されるとき、そこに含まれるシンボルは適切なスコープにおいてそのシンボルに束縛される値に置き換えられます。
パースで曖昧性を取り除くために、:
の引数に括弧が必要になることがあります:
julia> :(:)
:(:)
julia> :(::)
:(::)
式の評価
クオート
Julia の構文における :
の二つ目の用途は Expr
コンストラクタを使わない式オブジェクトの作成です。これはクオート (quote) と呼ばれます。文字 :
の後に単一の文を表す Julia コードを括弧に囲んで続けると、そのコードを表す Expr
オブジェクトが作成されます。この短い形式を使って算術式をクオートする例を示します:
julia> ex = :(a+b*c+1)
:(a + b * c + 1)
julia> typeof(ex)
Expr
式の構造を確認するには ex.head
や ex.args
を入力するか、上述の dump
または Meta.@dump
を使ってください。
同様の式は Meta.parse
または直接 Expr
を使っても構築できます:
julia> :(a + b*c + 1) ==
Meta.parse("a + b*c + 1") ==
Expr(:call, :+, :a, Expr(:call, :*, :b, :c), 1)
true
一般に、パーサーが生成する式は引数にシンボル・別の式・リテラル値だけを持ちます。これに対して Julia コードが生成する式は、リテラル形式を持たない実行時に計算される値を引数に持つことができます。上の例では +
と a
がシンボル、*(b,c)
が部分式、1
が 64 ビット符号付き整数リテラルです。
複数の式をクオートする構文もあります。quote ... end
でコードブロックを囲むと、内部にある式がクオートされます:
julia> ex = quote
x = 1
y = 2
x + y
end
quote
#= none:2 =#
x = 1
#= none:3 =#
y = 2
#= none:4 =#
x + y
end
julia> typeof(ex)
Expr
補間
値の引数を使った Expr
オブジェクトの構築は強力ですが、Expr
コンストラクタは "通常の" Julia の構文と比べると入力が面倒です。そこで Julia ではクオートされた式にリテラルと式を補間 (interpolation) できるようになっています。補間は $
で表します。
次の例では、変数 a
の値が補完されています:
julia> a = 1;
julia> ex = :($a + b)
:(1 + b)
クオートされていない式に対する補間はサポートされておらず、コンパイル時エラーとなります:
julia> $a + b
ERROR: syntax: "$" expression outside quote
次の例では、条件文の中にタプル (1,2,3)
が式として保管されています:
julia> ex = :(a in $:((1,2,3)) )
:(a in (1, 2, 3))
式の補間で $
を使うのは文字列の補間とコマンドの補間に意図的に似せてあります。式の補間を使えば、複雑な Julia 式をプログラムから簡単かつ読みやすい形で作成できます。
補間の展開
$
を使った補間で挿入できる式は一つだけです。しかしときには、式の配列に含まれる全ての式を周りの式の引数としたい場合もあります。これは $(xs...)
という構文で行えます。例えば次に示すのは、引数の数がプログラムから決定する関数呼び出しを作成するコードです:
julia> args = [:x, :y, :z];
julia> :(f(1, $(args...)))
:(f(1, x, y, z))
入れ子になったクオート
クオート式には他のクオート式を自然に入れられます。こういった場合の補間は少し理解しにくいかもしれません。次の例を考えます:
julia> x = :(1 + 2);
julia> e = quote quote $x end end
quote
#= none:1 =#
$(Expr(:quote, quote
#= none:1 =#
$(Expr(:$, :x))
end))
end
評価結果に $x
が含まれ、x
が評価されていないことに注目してください。言い換えると、この $
式が "属する" のは内側のクオート式であり、その引数は内側のクオート式が評価されたときに初めて評価されます:
julia> eval(e)
quote
#= none:1 =#
1 + 2
end
ただし、$
を複数重ねれば外側の quote
式でも補間を行えます:
julia> e = quote quote $$x end end
quote
#= none:1 =#
$(Expr(:quote, quote
#= none:1 =#
$(Expr(:$, :(1 + 2)))
end))
end
シンボル x
があった場所に (1 + 2)
が現れています。この式を評価すると、補間が行われて 3
となります:
julia> eval(e)
quote
#= none:1 =#
3
end
これを理解するには、x
が $
のたびに評価されると考えてください。$
を一つ付けると eval(:x)
のように動作し、$x
は x
の値で置き換わります。そして $
を二度付けると eval(eval(:x))
と同様となり、$$x
は 1 + 2
の値で置き換わります。
QuoteNode
quote
を使った式を AST で表現すると、通常は head
に :quote
を持つ Expr
となります:
julia> dump(Meta.parse(":(1+2)"))
Expr
head: Symbol quote
args: Array{Any}((1,))
1: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 2
上述の通り、こういった式は $
を使った補間をサポートします。しかしときには、補間を行わないクオートが必要になります。この種のクオートを表す構文はありませんが、内部では QuoteNode
型の値として表現されます:
julia> eval(Meta.quot(Expr(:$, :(1+2))))
3
julia> eval(QuoteNode(Expr(:$, :(1+2))))
:($(Expr(:$, :(1 + 2))))
パーサーはシンボルといったクオートされた単純な要素に対して QuoteNode
を生成します:
julia> dump(Meta.parse(":x"))
QuoteNode
value: Symbol x
QuoteNode
は一部の高度なメタプログラミングでも利用されます。
式の評価
式オブジェクトに対して eval
を使うと、Julia はグローバルスコープでそれを評価 (実行) します:
julia> :(1 + 2)
:(1 + 2)
julia> eval(ans)
3
julia> ex = :(a + b)
:(a + b)
julia> eval(ex)
ERROR: UndefVarError: b not defined
[...]
julia> a = 1; b = 2;
julia> eval(ex)
3
各モジュールはそれぞれが異なる eval
を持ちます。eval
は渡された式の評価だけではなく、それを含むモジュールの環境の状態を置き換える副作用を持つ処理も行えます:
julia> ex = :(x = 1)
:(x = 1)
julia> x
ERROR: UndefVarError: x not defined
julia> eval(ex)
1
julia> x
1
ここでは、式オブジェクトの評価がグローバル変数 x
への代入を行っています。
eval
は Expr
オブジェクトを評価しているだけであり、Expr
オブジェクトはプログラムを使っても作れるので、任意のコードを動的に生成して eval
することも可能です。この例を示します:
julia> a = 1;
julia> ex = Expr(:call, :+, a, :b)
:(1 + b)
julia> a = 0; b = 2;
julia> eval(ex)
3
a
の値は式 ex
を構築するのに使われ、構築される ex
は +
関数を 1
と変数 b
に適用します。a
と b
の使われ方の違いに気を付けてください:
- 変数
a
の値は式ex
を構築するのに使われ、a
の値1
が式ex
中の即値1
となります。そのため、式ex
が評価されるときのa
の値は式の値に関係しません: 式に含まれるのは1
であり、a
の値とは独立しています。 - これに対して
b
は式ex
の構築でシンボル:b
として使われているので、構築時点における変数b
の値は問題となりません ──:b
はただのシンボルであり、式ex
が構築された時点でb
は定義さえされていません。しかし式ex
を評価すると、シンボル:b
が変数b
の値を探索することで解決されます。
式に対する関数
上で簡単に触れた通り、Julia の非常に便利な機能の一つが Julia を使った Julia コードの生成・操作です。Expr
オブジェクトを返す関数はここまでに一つ示しています: parse
は Julia コードを表す文字列を受け取り、それが表す Expr
を返す関数です。関数は Expr
を引数として受け取って、何らかの処理を行った Expr
を返すこともできます。簡単で興味深い例を一つ示します:
julia> function math_expr(op, op1, op2)
expr = Expr(:call, op, op1, op2)
return expr
end
math_expr (generic function with 1 method)
julia> ex = math_expr(:+, 1, Expr(:call, :*, 4, 5))
:(1 + 4 * 5)
julia> eval(ex)
21
もう一つの例として、式に含まれる数値定数を倍にして式をそのままにする関数を示します:
julia> function make_expr2(op, opr1, opr2)
opr1f, opr2f = map(x -> isa(x, Number) ? 2*x : x, (opr1, opr2))
retexpr = Expr(:call, op, opr1f, opr2f)
return retexpr
end
make_expr2 (generic function with 1 method)
julia> make_expr2(:+, 1, 2)
:(2 + 4)
julia> ex = make_expr2(:+, 1, Expr(:call, :*, 5, 8))
:(2 + 5 * 8)
julia> eval(ex)
42
マクロ
マクロはプログラムによって生成されたコードをプログラムの本体に挿入するための仕組みです。マクロは引数のタプルを式に対応させます。マクロの返す式は実行時に直接コンパイルされ、そのとき eval
の呼び出しは必要となりません。マクロの引数には式・リテラル値・シンボルが利用できます。
基礎
非常に簡単なマクロを示します:
julia> macro sayhello()
return :( println("Hello, world!") )
end
@sayhello (macro with 1 method)
マクロを表す Julia の構文には専用の文字が用意されています: 最初に付いている '@'
です。@
の後に macro NAME ... end
ブロックで宣言されたユニークな名前が続くとマクロを表す名前となります。この例で言えば、コンパイラはコード内の @sayhello
の呼び出しを次の式で置き換えます:
:( println("Hello, world!") )
REPL で @sayhello()
を入力すると、マクロが返すこの式がすぐに実行されます。そのため私たちに見えるのは評価結果だけです:
julia> @sayhello()
Hello, world!
続いて、これより少し複雑なマクロを考えます:
julia> macro sayhello(name)
return :( println("Hello, ", $name) )
end
@sayhello (macro with 1 method)
このマクロは一つの引数 name
を受け取ります。コード中に @sayhello
が現れるとクオートされた式の展開が行われ、受け取った引数の値が式に補間されて最終的な式となります:
julia> @sayhello("human")
Hello, human
マクロが返すクオートされた式は macroexpand
で見ることができます (重要: これはマクロのデバッグで非常に便利なツールです):
julia> ex = macroexpand(Main, :(@sayhello("human")) )
:(Main.println("Hello, ", "human"))
julia> typeof(ex)
Expr
"human"
というリテラルが式に補完されているのが分かります。
@macroexpand
というマクロもあります。おそらく macroexpand
関数よりもこちらの方が便利でしょう:
julia> @macroexpand @sayhello "human"
:(println("Hello, ", "human"))
なぜマクロ?
前節では f(::Expr...) -> Expr
という型の関数を作れることを見ました。実は macroexpand
はそのような関数の一種です。では、なぜマクロが存在しているのでしょうか?
その理由は、パースされるときに実行されるマクロがあると、本筋のプログラムが実行される前にカスタマイズされたコードを生成してプログラム中に埋め込むことが可能になるからです。パース時と実行時の違いが際立つ例を次に示します:
julia> macro twostep(arg)
println("I execute at parse time. The argument is: ", arg)
return :(println("I execute at runtime. The argument is: ", $arg))
end
@twostep (macro with 1 method)
julia> ex = macroexpand(Main, :(@twostep :(1, 2, 3)) );
I execute at parse time. The argument is: :((1, 2, 3))
最初の println
の呼び出しは macroexpand
が呼ばれたときに実行されます。マクロが返す式には二つ目の println
の呼び出しだけが含まれます:
julia> typeof(ex)
Expr
julia> ex
:(println("I execute at runtime. The argument is: ", $(Expr(:copyast, :($(QuoteNode(:((1, 2, 3)))))))))
julia> eval(ex)
I execute at runtime. The argument is: (1, 2, 3)
マクロの起動
マクロは一般に次の構文で起動されます:
@name expr1 expr2 ...
@name(expr1, expr2, ...)
マクロの名前の前に付く @
に気を付けてください。また一つ目の書き方ではコンマを使ってはならず、二つ目の書き方では @name
の後に空白があってはいけません。
二つの書き方を混ぜることはできません。例えば次のコードでは (expr1, expr2, ...)
という一つの引数が @name
マクロに渡されるので、上のコードと違う意味になります:
@name (expr1, expr2, ...)
配列リテラル (もしくは内包表記) を渡してマクロを起動するときは、括弧を使わずに続けて書く記法が利用できます。この場合には配列がマクロへ渡される唯一の式となります。例えば次の二つの構文は等価であり、@name [a b] * v
と異なります:
@name[a b] * v
@name([a b]) * v
マクロの引数に利用できるのは式・リテラル・シンボルであることを強調しておきます。マクロに渡された引数を調べるには、show
関数をマクロ本体で使ってください:
julia> macro showarg(x)
show(x)
# ... 式を返す
end
@showarg (macro with 1 method)
julia> @showarg(a)
:a
julia> @showarg(1+1)
:(1 + 1)
julia> @showarg(println("Yo!"))
:(println("Yo!"))
明示的に与えられる引数リストに加えて、全てのマクロには __source__
と __module__
という引数が暗黙に与えられます。
引数 __source__
はマクロの起動に付く @
の位置を表すパーサーの位置情報を (LineNumberNode
オブジェクトとして) 提供します。__source__
を使えばマクロからエラーの詳しい診断情報を作成でき、他にもログ・文字列パーサーマクロ・ドキュメントなどでよく使われます。また @__LINE__
, @__FILE__
, @__DIR__
の実装にも __source__
は使われています。
マクロ呼び出しの位置情報は __source__.line
と __source__.file
で参照できます:
julia> macro __LOCATION__(); return QuoteNode(__source__); end
@__LOCATION__ (macro with 1 method)
julia> dump(
@__LOCATION__(
))
LineNumberNode
line: Int64 2
file: Symbol none
引数 __module__
はマクロの起動が展開される環境に関する情報を (Module
オブジェクトとして) 提供します。__module__
を使えば環境の情報 (例えば存在する束縛) にアクセスしたり、現在のモジュールに対して自己リフレクションを行って実行時の関数呼び出しに追加の引数として値を挿入したりできます。
高度なマクロ
Julia の @assert
マクロの定義を簡略化したものを示します:
julia> macro assert(ex)
return :( $ex ? nothing : throw(AssertionError($(string(ex)))) )
end
@assert (macro with 1 method)
このマクロは次のように使います:
julia> @assert 1 == 1.0
julia> @assert 1 == 0
ERROR: AssertionError: 1 == 0
コードに含まれるマクロの呼び出しは、パース時にそれが返す式に展開されます。つまり、この例は次のように書くのと等価です:
1 == 1.0 ? nothing : throw(AssertionError("1 == 1.0"))
1 == 0 ? nothing : throw(AssertionError("1 == 0"))
最初の呼び出しでは式 :(1 == 1.0)
がテストの条件節に埋め込まれ、さらに string(:(1 == 1.0))
がメッセージの引数部分に埋め込まれます。こうして構築される式は AST の @assert
マクロの呼び出しが起こった場所に配置されます。そして実行時に条件節が true
に評価されると nothing
が返り、false
に評価されるとエラーが発生して確認された式が false
であることを示すメッセージが表示されます。この処理は関数として実装できないことに注意してください。関数では条件の値しか利用できないので、その値を返した式をエラーメッセージに表示できません。
Julia Base にある実際の @assert
の定義はこれより複雑です。例えば失敗した式を出力するだけではなくユーザーからエラーメッセージを指定できます。可変長引数関数と同様に、マクロでも最後の引数にドットを三つ続けて可変長引数を表します:
julia> macro assert(ex, msgs...)
msg_body = isempty(msgs) ? ex : msgs[1]
msg = string(msg_body)
return :($ex ? nothing : throw(AssertionError($msg)))
end
@assert (macro with 1 method)
こうすると @assert
は受け取った引数の数に応じて二つのモードで動作します! 引数が一つのときは msgs
が受け取るタプルが空になるので、上で示した簡単な定義と同様となります。しかし二番目の引数を渡すと、失敗したときのメッセージとしてそれが表示されます。マクロの展開結果は @macroexpand
という分かりやすい名前のマクロで確認できます:
julia> @macroexpand @assert a == b
:(if Main.a == Main.b
Main.nothing
else
Main.throw(Main.AssertionError("a == b"))
end)
julia> @macroexpand @assert a==b "a should equal b!"
:(if Main.a == Main.b
Main.nothing
else
Main.throw(Main.AssertionError("a should equal b!"))
end)
Julia Base の @assert
マクロが処理できるケースがもう一つあります: "a should equal b" と表示するのに加えて、a
と b
の値も表示したいとしたら? 文字列の補間を使って @assert a==b "a ($a) should equal b ($b)!"
とするのが素朴な方法として考えられますが、これは上記のマクロでは上手く行きません。上手く行かない理由が分かるでしょうか? 文字列の補間で説明したように、補完された文字列は string
への呼び出しに書き換わります。次の結果を比較してください:
julia> typeof(:("a should equal b"))
String
julia> typeof(:("a ($a) should equal b ($b)!"))
Expr
julia> dump(:("a ($a) should equal b ($b)!"))
Expr
head: Symbol string
args: Array{Any}((5,))
1: String "a ("
2: Symbol a
3: String ") should equal b ("
4: Symbol b
5: String ")!"
この例から分かるように、補間を使ったときに上記のマクロが受け取る msg_body
は文字列ではなく完全な式なので、期待通りに表示させるには評価が必要です。つまり msg_body
をマクロが返す式に直接埋め込めば、それが string
の呼び出しとなって評価が行われます。完全な実装は error.jl
にあります。
@assert
マクロはクオートした式に対して式を埋め込む機能を利用することで、マクロ本体における式の操作を簡単にしています。
マクロの健全性
さらに複雑なマクロでは、マクロの健全性 (hygiene) が問題になります。簡単に言うと、マクロが返す式で作成される変数が展開したコードに含まれる既存の変数と誤って衝突しないことを保証しなければなりません。反対に、引数としてマクロに渡される式は呼び出したコードのコンテキストで既存の変数を利用・変更しながら実行されるものとして渡されます。
マクロが定義されたモジュールとは別のモジュールから呼び出せるという事実からも別の問題が発生します。この場合、全てのグローバル変数を正しいモジュールを使って解決しなければなりません。Julia ではマクロが返す式だけを考えればよいので、(C のような) 文字的なマクロ展開を行う言語ほどには問題となりません。マクロが返す式に含まれない変数 (例えば上記の @assert
に含まれる msg
) は通常のブロックと同じスコープ規則で扱われます。
こういった問題の例を示すために、@time
マクロを考えます。このマクロは引数に式を受け取り、時刻を記録し、式を評価し、時刻をもう一度記録し、二つの時刻の差を出力し、式の値を最終的な値として返します。このマクロは次のような形をするはずです:
macro time(ex)
return quote
local t0 = time_ns()
local val = $ex
local t1 = time_ns()
println("elapsed time: ", (t1-t0)/1e9, " seconds")
val
end
end
ここで t0
, t1
, val
はプライベートな一時変数であり、time_ns
は Julia Base に含まれる time_ns
関数を指しています。このマクロを使うユーザーが変数 time_ns
を定義していたとしても、それではありません (同じことは println
にも言えます)。今考えているのは式 ex
が変数 t0
へ代入していたり、独自に time_ns
変数を定義していた場合の問題です。このとき何もしなければエラーや間違った動作が起こる可能性があります。
Julia のマクロ展開はこの問題を次のように解決します。まずマクロの返り値に含まれる変数はローカルとグローバルに分けられます。変数がローカルなのは (グローバルと宣言されずに) 代入されるとき、ローカルと宣言されるとき、そして関数の引数として使われるときで、これ以外は全てグローバルです。ローカル変数はその後 (gensym
関数を使って) ユニークな名前に変更され、グローバルな変数はマクロが定義された環境で解決されます。これにより二つの問題点は解決されます: マクロのローカル変数はユーザーの変数と衝突せず、time_ns
と println
は Julia Base の定義を参照します。
ただ問題は一つ残ります。このマクロを次のように使ったとします:
module MyModule
import Base.@time
time_ns() = ... # 何かを計算する
@time time_ns()
end
このときユーザーが与える式 ex
が time_ns
の呼び出しとなりますが、もちろんこの time_ns
は MyModule.time_ns
を指しています。よって ex
に含まれる変数についてはマクロを呼び出した側の環境で解決されるよう調整が必要です。この調整は esc
で式を "エスケープ" すると行えます:
macro time(ex)
...
local val = $(esc(ex))
...
end
このように esc
で囲った式はマクロの展開処理でそのまま出力にペーストされるので、名前の解決はマクロの呼び出し側の環境で行われます。
必要な場合にはこの esc
を使ったエスケープ処理を使ってマクロの健全性を破るような変数の生成・操作も可能です。例えば、次のマクロは呼び出し側の環境における変数 x
を 0
に設定します:
julia> macro zerox()
return esc(:(x = 0))
end
@zerox (macro with 1 method)
julia> function foo()
x = 1
@zerox
return x # 0 になる。
end
foo (generic function with 1 method)
julia> foo()
0
この種の変数の操作はよく考えて使わなければなりませんが、一部の状況で非常に活躍します。
マクロを健全性を保ったまま正しく使うのが手ごわい問題になることがあります。マクロを使う前に、関数クロージャが使えないかを考えてみるとよいかもしれません。もう一つの有効な戦略は、仕事をできる限り実行時まで遅らせることです。例えば、引数を QuoteNode
やこれに似た簡単な Expr
で包むだけのマクロが多くあります。例えば @task body
は schedule(Task(() -> $body))
を返すだけであり、@eval expr
は eval(QuoteNode(expr))
を返すだけです。
上記の @time
をこの考え方に基づいて書き換えた例を示します:
macro time(expr)
return :(timeit(() -> $(esc(expr))))
end
function timeit(f)
t0 = time_ns()
val = f()
t1 = time_ns()
println("elapsed time: ", (t1-t0)/1e9, " seconds")
return val
end
ただ、こうすべきでない十分な理由があるので、実際にはこうなっていません。expr
を新しいスコープブロック (無名関数) で包むと、式の意味 (ブロックに含まれる変数のスコープ) が少し変わってしまうからです。@time
は引数のコードに対する影響を最小限にする必要があります。
マクロとディスパッチ
Julia のマクロは関数と同様に「総称 (generic)」です。これは一つのマクロに複数のメソッドを定義を与え、呼び出すメソッドを多重ディスパッチで選択できることを意味します:
julia> macro m end
@m (macro with 0 methods)
julia> macro m(args...)
println("$(length(args)) arguments")
end
@m (macro with 1 method)
julia> macro m(x,y)
println("Two arguments")
end
@m (macro with 2 methods)
julia> @m "asd"
1 arguments
julia> @m 1 2
Two arguments
ただし、マクロのディスパッチではマクロに渡された AST の型が使われ、AST が実行時に評価される値の型は使われないことに注意が必要です:
julia> macro m(::Int)
println("An Integer")
end
@m (macro with 3 methods)
julia> @m 2
An Integer
julia> x = 2
2
julia> @m x
1 arguments
コード生成
ボイラープレートコードが何度も繰り返されるときは、プログラムを使ってコードを生成して重複を避けるのが一般的です。多くの言語ではコードの生成に追加のビルドステップが必要になり、他のプログラムが反復的なコードを生成します。しかし Julia には式の補間と eval
があるので、そういったコード生成を通常のプログラム実行の中に入れることができます。例えば、次の独自型を定義したとします:
struct MyNumber
x::Float64
end
この型に対して様々なメソッドを追加するときは、次のループでプログラムから行えます:
for op = (:sin, :cos, :tan, :log, :exp)
eval(quote
Base.$op(a::MyNumber) = MyNumber($op(a.x))
end)
end
こうすれば、この独自型に対するこれらの関数を全て利用できます:
julia> x = MyNumber(π)
MyNumber(3.141592653589793)
julia> sin(x)
MyNumber(1.2246467991473532e-16)
julia> cos(x)
MyNumber(-1.0)
このように使うとき Julia は自身に対するプリプロセッサとなり、同じ言語内でコード生成が行えます。上記のコードは :
を前に付けてクオートする記法を使えば少し簡単に書けます:
for op = (:sin, :cos, :tan, :log, :exp)
eval(:(Base.$op(a::MyNumber) = MyNumber($op(a.x))))
end
ただし、この種の eval(quote(...))
パターンを使った言語内からのコード生成は Julia で非常によく使われるので、Julia にはこれを短縮して書くためのマクロがあります:
for op = (:sin, :cos, :tan, :log, :exp)
@eval Base.$op(a::MyNumber) = MyNumber($op(a.x))
end
@eval
マクロはこの呼び出しを一つ前の長いバージョンと全く同じ式に書き換えます。生成されるコードが長い場合には、引数の式をブロックとして渡してください:
@eval begin
# 複数行のコード
end
非標準文字列リテラル
文字列の章で説明したように、先頭に識別子が付いた文字列リテラルは非標準文字列リテラルと呼ばれ、先頭に何も付いていない通常の文字列リテラルとは異なる意味論を持つことができます。いくつか例を示します:
r"^\s*(?:#|$)"
は文字列ではなく正規表現を生成します。b"DATA\xff\u2200"
はバイト配列リテラルであり、[68,65,84,65,255,226,136,128]
を意味します。
驚くかもしれませんが、標準文字列リテラルの動作は Julia のパーサーやコンパイラにハードコードされているわけではありません。そうではなく、これらの動作は誰でも利用できる一般的なメカニズムを使って提供されます。つまり、識別子が先頭に付いた文字列リテラルはその識別子の名前を使った特別な名前のマクロの呼び出しとしてパースされます。例えば、正規表現を作成する非標準文字列リテラルのためのマクロ定義は次の通りです:
macro r_str(p)
Regex(p)
end
たったこれだけです。このマクロによって r"^\s*(?:#|$)"
のような非標準文字列リテラルのリテラル部分は @r_str
マクロに渡され、その返り値が AST の非標準文字列リテラルがあった場所に配置されるようになります。言い換えると、r"^\s*(?:#|$)"
という式は次のオブジェクトを AST に配置するのと等価です:
Regex("^\\s*(?:#|\$)")
このリテラルは短くて使いやすいだけではなく、性能面でも優れています。コードがコンパイルされるときに正規表現もコンパイルされて Regex
オブジェクトが作成されるので、リテラルが何度実行されようと正規表現のコンパイルは一度しか起きません。正規表現がループに含まれる状況を考えてみてください:
for line = lines
m = match(r"^\s*(?:#|$)", line)
if m === nothing
# non-comment
else
# comment
end
end
正規表現 r"^\s*(?:#|$)"
はコードがパースされるときにコンパイルされ AST に挿入されるので、この正規表現は一度だけコンパイルされます。ループの反復のたびにコンパイルされるようなことはありません。マクロを使わずにこの動作を実現するには、ループの外にリテラルを持っていくしかありません:
re = Regex("^\\s*(?:#|\$)")
for line = lines
m = match(re, line)
if m === nothing
# non-comment
else
# comment
end
end
さらに、正規表現オブジェクトがループを通じて定数であることをコンパイラが見つけられないと、一部の最適化が不可能になる可能性があります。もしそうなると、マクロを使わずに書いたバージョンはマクロを使った書きやすいバージョンより性能が落ちることになります。もちろんリテラルでない形の方が使いやすい状況も存在します: 実行時に変数を正規表現に補完する場合には、この手間のかかるアプローチが必要です。また正規表現が動的で反復ごとに更新されるなら、正規表現オブジェクトは毎回新しく作成しなければなりません。しかし大部分のケースでは、正規表現の作成に実行時のデータは使われません。こういったケースでは正規表現をコンパイル時に計算できる機能が大きな価値を持ちます。
非標準文字列リテラルと同様に、コマンドリテラルの先頭に識別子を付けた非標準コマンドリテラルも存在します。例えば custom`literal`
という非標準コマンドリテラルは @custom_cmd "literal"
とパースされます。Julia に組み込みの非標準コマンドリテラルはありませんが、パッケージからは利用できます。記法が違うこととマクロの名前が ..._str
ではなく ..._cmd
であることを除けば、非標準コマンドリテラルは非標準文字列リテラルと全く同じように動作します。
二つのモジュールが同じ名前の非標準の文字列リテラルまたはコマンドリテラルを提供しているときは、リテラルの先頭に付いている識別子にモジュールの名前を付けることができます。例えば Foo
と Bar
の両方が非標準文字列リテラル @x_str
を提供しているなら、Foo.x"literal"
と Bar.x"literal"
を使って二つを区別します。
ユーザー定義の文字列リテラルは Julia の様々な機能が深く絡む非常に強力な機能です。Julia に標準で実装されている非標準リテラルがこの機能を使っているだけではなく、コマンドリテラルの記法 (`echo "Hello, $person"`
) は次の何の変哲もないマクロで実装されます:
macro cmd(str)
:(cmd_gen($(shell_parse(str)[1])))
end
もちろん、このマクロの定義に含まれる関数には多くの複雑さが隠されていますが、全て Julia で書かれた関数に過ぎません。何が起こっているかはソースコードを見れば完全に分かります ──このマクロが行うのは、式オブジェクトを構築して AST に挿入するというただそれだけの処理です。
被生成関数
@generated
は被生成関数 (generated function) を生成する非常に特殊なマクロです。被生成関数を使うと、引数の型に基づいた柔軟なコード生成が多重ディスパッチよりも少ないコードで可能になります。マクロはパース時に式に対して処理を行うので入力の型にアクセスできませんが、被生成関数は引数の型が分かっているときに呼び出され、引数の型ごとに一度だけコンパイルされます。
被生成関数は計算や処理を行うのではなく、引数の型に対応するメソッドの本体を表すクオートされた式を返します。被生成関数を呼び出すと、返り値の式がコンパイルされて実行されます。被生成関数では効率を高めるために結果の式が通常キャッシュされます。また型の推論が行えるように、一部の言語機能しか使えません。つまり被生成関数は利用できる構文を犠牲にすることで実行時の処理をコンパイル時に移動させる柔軟な方法を提供します。
被生成関数の定義と通常の関数の定義の間には、五つの大きな違いがあります:
- 被生成関数の宣言には
@generated
という注釈を付けます。こうすると AST に情報が加わり、定義されるのが被生成関数であることがコンパイラに伝わります。 - 被生成関数の本体からアクセスできるのは引数の型だけです ──引数の値にはアクセスできません。
- 被生成関数は何らかの値を計算したり何らかの処理を行ったりするのではなく、評価すると行うべき計算や処理が行われるクオートされた式を返します。
- 被生成関数は自身の定義より前に定義された関数だけを呼び出せます。自身の定義より後に定義された関数を呼び出すと、未来の世界時を持つ関数を呼び出しているという
MethodErrors
が発生します。 - 被生成関数は定数でないグローバルな状態を観測・改変できません (例えば IO・ロック・ローカルでない辞書・
hasmethod
はどれも使えません)。被生成関数は外部状態としてグローバルな定数だけを読むことができ、副作用を持てないことをこれは意味します。言い換えると、被生成関数は完全に純粋でなければなりません。また実装の制限により、現在の被生成関数ではクロージャとジェネレータを定義できません。
例を見るのが分かりやすいでしょう。被生成関数 foo
は次のように宣言します:
julia> @generated function foo(x)
Core.println(x)
return :(x * x)
end
foo (generic function with 1 method)
本体が返しているのが値 x * x
ではなく :(x * x)
というクオートされた式であることに注目してください。
呼び出す側から見ると、foo
は普通の関数と何ら変わりません。実を言えば、呼び出しているのが通常の関数か被生成関数かを知っておく必要はありません。foo
の振る舞いを示します:
julia> x = foo(2); # 出力は被生成関数の本体に含まれる println() によるもの
Int64
julia> x # x を出力する
4
julia> y = foo("bar");
String
julia> y
"barbar"
この例から分かるように、被生成関数の本体において x
は渡された引数の型となります。そして被生成関数が返す値は、本体が返すクオートされた式を x
の値を使って評価した結果です。
一度使った型の値を使って foo
を再度評価するとどうなるでしょうか?
julia> foo(4)
16
このときは Int64
が出力されません。つまり関数の本体は引数の型ごとにキャッシュされ、通常は一度だけ実行されます。以降の実行ではこの例のように被生成関数が最初に返した式がメソッドの本体として再利用されます。ただし、実際のキャッシュの動作は実装定義の性能最適化なので、この動作に完全に依存してはいけません。
被生成関数が生成される回数は一度だけである可能性もありますが、二度以上になる可能性も、一度も起こらないように見える可能性もあります。そのため、副作用を持った被生成関数は絶対に書くべきではありません ──副作用が起こるタイミングが未定義となるからです (これはマクロでも同様です)。またマクロと同様に、被生成関数で eval
が必要になったとしたら、何かを間違った方法で行っているサインです。ただしマクロとは異なり、ランタイムシステムは被生成関数に含まれる eval
を正しく処理できないので、最初から無効化されています。
メソッドの再定義に対する被生成関数の振る舞いを理解しておくことも重要です。正しい被生成関数はグローバル状態の観測・改変を行わないので、次に示す動作となります。被生成関数は自身の定義よりも前に定義されていない関数を呼び出せないことに注意してください。
最初 f(x)
は一つの定義を持つとします:
julia> f(x) = "original definition";
f(x)
を利用する関数と被生成関数を定義します:
julia> g(x) = f(x);
julia> @generated gen1(x) = f(x);
julia> @generated gen2(x) = :(f(x));
さらに f(x)
に定義を加えます:
julia> f(x::Int) = "definition for Int";
julia> f(x::Type{Int}) = "definition for Type{Int}";
このとき結果が異なることを確認してください:
julia> f(1)
"definition for Int"
julia> g(1)
"definition for Int"
julia> gen1(1)
"original definition"
julia> gen2(1)
"definition for Int"
被生成関数の各メソッドから見える関数はそれぞれ異なります:
julia> @generated gen1(x::Real) = f(x);
julia> gen1(1)
"definition for Type{Int}"
上記の被生成関数 foo
は (初めて呼び出されたときに型を出力することと、オーバーヘッドが大きいことを除けば) 通常の関数 foo(x) = x * x
と同じことしかしていません。被生成関数の力が発揮されるのは、型に応じて異なるクオートされた式を計算するときです:
julia> @generated function bar(x)
if x <: Integer
return :(x ^ 2)
else
return :(x)
end
end
bar (generic function with 1 method)
julia> bar(4)
16
julia> bar("baz")
"baz"
(もちろんこの例にしても多重ディスパッチを使えば簡単に実装できるのですが...)
この機能を悪用すれば未定義動作を発生させることができます:
julia> @generated function baz(x)
if rand() < .9
return :(x^2)
else
return :("boo!")
end
end
baz (generic function with 1 method)
この被生成関数の本体は非決定的なので、その動作 (およびこの関数を呼び出すコードの動作) は未定義となります。
ここまでの例をコピーしないこと!
被生成関数がどう定義されどう呼ばれるかをこの節の例で理解できたことを願います。ただし、このような被生成関数は書かないでください。理由は次の通りです:
foo
関数には副作用 (Core.println
の呼び出し) があります。この副作用がいつ呼び出されるか、あるいは何回呼び出されるかは未定義です。bar
関数が解決している問題は多重ディスパッチを使った方が簡単に解決できます。bar(x) = x
およびbar(x::Integer) = x ^ 2
と定義すれば同じであり、この方が単純で高速です。baz
は病的です。
被生成関数で行うべきでない処理は無数にあるのに対して、現在のランタイムシステムが検出できる無効な処理は限られていることに注意してください。実行するとランタイムシステムが警告を出さずに壊れる操作も多くあり、そういった操作はたいてい定義からは明らかでない細かな部分に隠れています。関数ジェネレータは型推論時に実行されるので、被生成関数はコードの制限を遵守しなければなりません。
被生成関数で行うべきでない処理の例を示します:
-
ネイティブポインタをキャッシュする。
-
何らかの形で
Core.Compiler
のメソッドに触れる。 -
可変な状態を観測する。
- 被生成関数に対する推論は任意のタイミングで実行されるので、コードが可変な状態を観測・改変している途中に実行される可能性があります。
-
ロックを取得する: 呼び出した C コードがロックを使うのは許されます (例えば
malloc
は内部でロックを必要としますが、これを呼び出すのは構いません)。しかし実行される Julia コードからロックを取得・保持しないでください。 -
被生成関数の本体より後に定義された関数を呼ぶことは許されません。漸進的に読み込まれる事前コンパイル済みモジュールではこの制限は緩められ、被生成関数はモジュール内の任意の関数を呼び出せます。
被生成関数の動作について理解が深まったと思うので、次は高度な機能を作るのに被生成関数を (正しく) 使ってみましょう...。
高度な例
Julia Base ライブラリは内部で sub2ind
という関数を使って n 次元配列に対する線形の添え字を n 個の次元から計算します。言い換えると、sub2ind
関数は A[x,y,z,...]
という配列へのアクセスが A[i]
と同じになるような i
を計算します。この処理は通常の関数としても実装できます:
julia> function sub2ind_loop(dims::NTuple{N}, I::Integer...) where N
ind = I[N] - 1
for i = N-1:-1:1
ind = I[i]-1 + dims[i]*ind
end
return ind + 1
end
sub2ind_loop (generic function with 1 method)
julia> sub2ind_loop((3, 5), 1, 2)
4
再帰を使っても書けます:
julia> sub2ind_rec(dims::Tuple{}) = 1;
julia> sub2ind_rec(dims::Tuple{}, i1::Integer, I::Integer...) =
i1 == 1 ? sub2ind_rec(dims, I...) : throw(BoundsError());
julia> sub2ind_rec(dims::Tuple{Integer, Vararg{Integer}}, i1::Integer) = i1;
julia> sub2ind_rec(dims::Tuple{Integer, Vararg{Integer}}, i1::Integer, I::Integer...) =
i1 + dims[1] * (sub2ind_rec(Base.tail(dims), I...) - 1);
julia> sub2ind_rec((3, 5), 1, 2)
4
この二つの異なる実装が行う処理は本質的に同じです: 実行時に配列の次元をループして各次元のオフセットを収集し、最終的なオフセットを計算しています。
しかし、このループで必要になる情報は引数の型情報に全て含まれています。そのため、被生成関数を使えばループをコンパイル時に移動させることができます。コンパイラの用語で言い換えると、被生成関数を使った手動のループ展開が可能です。次の被生成関数の本体は上で示したものとほぼ同じですが、線形の添え字を計算する代わりに、線形の添え字を計算する式を計算します:
julia> @generated function sub2ind_gen(dims::NTuple{N}, I::Integer...) where N
ex = :(I[$N] - 1)
for i = (N - 1):-1:1
ex = :(I[$i] - 1 + dims[$i] * $ex)
end
return :($ex + 1)
end
sub2ind_gen (generic function with 1 method)
julia> sub2ind_gen((3, 5), 1, 2)
4
このコードは何を生成する?
被生成関数の本体を (通常の) 関数に切り出すと理解しやすくなります:
julia> @generated function sub2ind_gen(dims::NTuple{N}, I::Integer...) where N
return sub2ind_gen_impl(dims, I...)
end
sub2ind_gen (generic function with 1 method)
julia> function sub2ind_gen_impl(dims::Type{T}, I...) where T <: NTuple{N,Any} where N
length(I) == N || return :(error("partial indexing is unsupported"))
ex = :(I[$N] - 1)
for i = (N - 1):-1:1
ex = :(I[$i] - 1 + dims[$i] * $ex)
end
return :($ex + 1)
end
sub2ind_gen_impl (generic function with 1 method)
sub2ind_gen_impl
を実行して何が返るかを見てみましょう:
julia> sub2ind_gen_impl(Tuple{Int,Int}, Int, Int)
:(((I[1] - 1) + dims[1] * (I[2] - 1)) + 1)
この式にはループが一切含まれていません ──二つのタプルに対する添え字アクセスが、算術演算で組み合わさっているだけです。ループは全てコンパイル時に行われるので、実行時のループは必要ありません。被生成関数は引数の型に対して一度ずつ呼び出されるので、今考えている例では N
の値ごとにループの計算が行われます (被生成関数が複数回生成される場合は除きます ──上述の注意を参照してください)。
随時被生成関数 (optionally-generated function)
被生成関数は実行時における高い性能を達成しますが、コンパイル時のコストがかさみます: 新しい関数の本体は全ての具象型の組み合わせに対して生成されなければなりません。通常の関数に対してであれば Julia はどんな引数に対しても動作する "汎用" バージョンの関数をコンパイルできるのですが、被生成関数に対してはこの処理を行えません。これは被生成関数を多用するプログラムの静的コンパイルが事実上不可能になるかもしれないことを意味します。
この問題を解決するために、Julia には被生成関数に対して被生成でない通常の実装を提供するための構文が存在します。上記の sub2ind
に適用すると、次のようになります:
function sub2ind_gen(dims::NTuple{N}, I::Integer...) where N
if N != length(I)
throw(ArgumentError("Number of dimensions must match number of indices."))
end
if @generated
ex = :(I[$N] - 1)
for i = (N - 1):-1:1
ex = :(I[$i] - 1 + dims[$i] * $ex)
end
return :($ex + 1)
else
ind = I[N] - 1
for i = (N - 1):-1:1
ind = I[i] - 1 + dims[i]*ind
end
return ind + 1
end
end
このコードは関数に対する二つの実装を内部に作成します: 一つは if @generated
から else
までのブロックからなる被生成のコードで、もう一つは else
から end
までのブロックからなる通常のコードです。if @generated
ブロックのコードは通常の被生成関数と同じ意味論を持ちます: 引数は型を指し、この関数は式を返すべきです。if @generated
ブロックが複数含まれていても構わず、そのときは全ての if @generated
ブロックが使われるか全ての else
ブロックが使われるかのどちらかとなります。
エラーチェックを関数の最初に追加していることに注目してください。このコードは二つのバージョンで共通であり、両方で実行時に実行されます (被生成バージョンではクオートされた上で関数で生成された式として返ります)。これはコード生成時にはローカル変数の型と値が利用できないことを意味します ──コードを生成するコードが見れるのは引数の型だけです。
この形式で関数を定義すると、コード生成の機能は本質的に省略可能な最適化として扱われます。コンパイラは気が向けばコード生成を使うかもしれませんが、そうでなければ通常の実装を使うかもしれません。この形式を使えばコンパイラがプログラムをコンパイルするときの選択肢が増え、さらにコード生成のコードがより読みやすくなります。そのためこの形式が推奨されますが、どちらの実装を使うかはコンパイラの実装に任されます。よって二つの実装が同じ動作をすることは非常に重要です。