Metaprogramming
Lispの最も強力な遺産は、Julia言語におけるメタプログラミングのサポートです。Lispと同様に、Juliaは自分自身のコードを言語自体のデータ構造として表現します。コードは言語内から作成および操作できるオブジェクトによって表現されるため、プログラムが自分自身のコードを変換および生成することが可能です。これにより、追加のビルドステップなしで高度なコード生成が可能になり、また、abstract syntax treesのレベルで動作する真のLispスタイルのマクロも可能になります。それに対して、CやC++のようなプリプロセッサの「マクロ」システムは、実際の解析や解釈が行われる前にテキストの操作や置換を行います。Juliaでは、すべてのデータ型とコードがJuliaのデータ構造によって表現されるため、プログラムとその型の内部を探るための強力なreflection機能が利用可能です。
メタプログラミングは強力なツールですが、コードを理解しにくくする複雑さを導入します。例えば、スコープルールを正しく取得するのは驚くほど難しい場合があります。メタプログラミングは、通常、higher order functionsやclosuresなどの他のアプローチが適用できない場合にのみ使用されるべきです。
eval と新しいマクロの定義は、通常、最後の手段として使用されるべきです。Meta.parse を使用したり、任意の文字列を Julia コードに変換することは、ほとんど良いアイデアではありません。Julia コードを操作するには、Julia の構文がどのように解析されるかの複雑さを避けるために、Expr データ構造を直接使用してください。
メタプログラミングの最良の使用法は、しばしばランタイムヘルパー関数にほとんどの機能を実装し、生成するコードの量を最小限に抑えることを目指します。
Program representation
すべてのJuliaプログラムは文字列として始まります:
julia> prog = "1 + 1"
"1 + 1"次に何が起こるのか?
次のステップは、parse 各文字列を、Julia型 Expr で表されるオブジェクト「expression」に変換することです。
julia> ex1 = Meta.parse(prog)
:(1 + 1)
julia> typeof(ex1)
ExprExpr オブジェクトは二つの部分を含みます:
- a
Symbolは、表現の種類を特定します。シンボルは interned string 識別子です(以下でさらに議論します)。
julia> ex1.head
:call- 式の引数は、シンボル、他の式、またはリテラル値である可能性があります:
julia> ex1.args
3-element Vector{Any}:
:+
1
1式は prefix notation で直接構築することもできます:
julia> ex2 = Expr(:call, :+, 1, 1)
:(1 + 1)上記で構築された2つの表現 – パースによるものと直接構築によるもの – は同等です:
julia> ex1 == ex2
trueここでの重要な点は、Juliaコードが内部的にデータ構造として表現されており、そのデータ構造に言語自体からアクセスできるということです。
dump 関数は、Expr オブジェクトのインデントされた注釈付き表示を提供します:
julia> dump(ex2)
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 1Expr オブジェクトはネストすることもできます:
julia> ex3 = Meta.parse("(4 + 4) / 2")
:((4 + 4) / 2)別の方法として、Meta.show_sexprを使用して式を表示することができます。これは、与えられたExprのS-expression形式を表示します。この形式はLispのユーザーには非常に馴染みがあるかもしれません。以下は、ネストされたExprの表示を示す例です:
julia> Meta.show_sexpr(ex3)
(:call, :/, (:call, :+, 4, 4), 2)Symbols
: 文字は、Julia において二つの構文的目的を持っています。最初の形式は、Symbol を作成します。これは、式の構成要素の一つとして使用される interned string です。
julia> s = :foo
:foo
julia> typeof(s)
SymbolSymbol コンストラクタは、任意の数の引数を受け取り、それらの文字列表現を連結することによって新しいシンボルを作成します:
julia> :foo === Symbol("foo")
true
julia> Symbol("1foo") # `:1foo` would not work, as `1foo` is not a valid identifier
Symbol("1foo")
julia> Symbol("func",10)
:func10
julia> Symbol(:var,'_',"sym")
:var_sym式の文脈では、シンボルは変数へのアクセスを示すために使用されます。式が評価されると、シンボルは適切な scope でそのシンボルにバインドされた値に置き換えられます。
時々、解析の曖昧さを避けるために、: への引数の周りに余分な括弧が必要です:
julia> :(:)
:(:)
julia> :(::)
:(::)Expressions and evaluation
Quoting
: 文字の第二の構文目的は、明示的な Expr コンストラクタを使用せずに式オブジェクトを作成することです。これは 引用 と呼ばれます。: 文字の後に、Julia コードの単一のステートメントを囲むペアの括弧を続けると、囲まれたコードに基づいて Expr オブジェクトが生成されます。以下は、算術式を引用するために使用される短い形式の例です:
julia> ex = :(a+b*c+1)
:(a + b * c + 1)
julia> typeof(ex)
Expr(to view the structure of this expression, try ex.head and ex.args, or use dump as above or 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)
ExprInterpolation
Expr オブジェクトを値引数で直接構築することは強力ですが、Expr コンストラクタは「通常の」Julia構文と比べて面倒です。代わりに、Juliaはリテラルや式を引用された式に補間することを許可しています。補間はプレフィックス $ で示されます。
この例では、変数 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))$を使った式の補間は、意図的にstring interpolationやcommand interpolationを思い起こさせるものです。式の補間は、複雑なJulia式をプログラム的に便利かつ読みやすく構築することを可能にします。
Splatting interpolation
$ 補間構文は、囲まれた式に単一の式のみを挿入することを許可することに注意してください。時には、式の配列があり、それらすべてを周囲の式の引数にする必要があります。これは $(xs...) 構文を使用することで実現できます。たとえば、以下のコードは、引数の数がプログラム的に決定される関数呼び出しを生成します。
julia> args = [:x, :y, :z];
julia> :(f(1, $(args...)))
:(f(1, x, y, z))Nested quote
当然、引用式が他の引用式を含むことは可能です。これらのケースでの補間がどのように機能するかを理解するのは少し難しいかもしれません。この例を考えてみましょう:
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(1 + 2)が結果に表示され、シンボルxの代わりになっていることに注意してください。この式を評価すると、補間された3が得られます:
julia> eval(e)
quote
#= none:1 =#
3
endこの動作の背後にある直感は、x が各 $ ごとに一度評価されるということです:1つの $ は eval(:x) と同様に動作し、x の値を返しますが、2つの $ は eval(eval(:x)) と同等の動作をします。
QuoteNode
The usual representation of a quote form in an AST is an Expr with head :quote:
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 xQuoteNode は、特定の高度なメタプログラミングタスクにも使用できます。
Evaluating expressions
与えられた式オブジェクトを使用すると、Juliaにそれをグローバルスコープで評価(実行)させることができます eval:
julia> ex1 = :(1 + 2)
:(1 + 2)
julia> eval(ex1)
3
julia> ex = :(a + b)
:(a + b)
julia> eval(ex)
ERROR: UndefVarError: `b` not defined in `Main`
[...]
julia> a = 1; b = 2;
julia> eval(ex)
3すべての module は、それぞれ独自の eval 関数を持ち、グローバルスコープ内で式を評価します。 4d61726b646f776e2e436f64652822222c20226576616c2229_40726566 に渡される式は、値を返すことに制限されず、囲むモジュールの環境の状態を変更する副作用を持つこともあります。
julia> ex = :(x = 1)
:(x = 1)
julia> x
ERROR: UndefVarError: `x` not defined in `Main`
julia> eval(ex)
1
julia> x
1ここでは、式オブジェクトの評価がグローバル変数 x に値を割り当てる原因となります。
式は単に Expr オブジェクトであり、プログラム的に構築して評価することができるため、任意のコードを動的に生成し、eval を使用して実行することが可能です。以下は簡単な例です:
julia> a = 1;
julia> ex = Expr(:call, :+, a, :b)
:(1 + b)
julia> a = 0; b = 2;
julia> eval(ex)
3aの値は、値1と変数bに+関数を適用する式exを構築するために使用されます。aとbの使われ方の重要な違いに注意してください:
- 式の構築時における 変数
aの値は、式内で即時値として使用されます。したがって、式が評価されるときのaの値はもはや重要ではありません:式内の値はすでに1であり、aの値が何であれ独立しています。 - 一方で、シンボル
:bは式の構築に使用されるため、その時点での変数bの値は無関係です –:bは単なるシンボルであり、変数bは定義されている必要すらありません。しかし、式の評価時には、シンボル:bの値は変数bの値を調べることによって解決されます。
Functions on Expressions
上記のように示唆されているように、Juliaの非常に便利な機能の一つは、Julia自身の中でJuliaコードを生成し、操作する能力です。私たちはすでに、Exprオブジェクトを返す関数の一例を見ました。それは、文字列のJuliaコードを受け取り、対応するExprを返すMeta.parse関数です。関数は、1つ以上の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別の例として、数値の引数を2倍にする関数を示しますが、式はそのままにします:
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)
42Macros
マクロは、プログラムの最終的な本体に生成されたコードを含めるためのメカニズムを提供します。マクロは、引数のタプルを返される式にマッピングし、結果の式はランタイムの eval 呼び出しを必要とせずに直接コンパイルされます。マクロの引数には、式、リテラル値、およびシンボルが含まれる場合があります。
Basics
ここに非常にシンプルなマクロがあります:
julia> macro sayhello()
return :( println("Hello, world!") )
end
@sayhello (macro with 1 method)マクロは、Juliaの構文において専用の文字を持っています:@(アットサイン)の後に、macro NAME ... endブロックで宣言されたユニークな名前が続きます。この例では、コンパイラはすべての@sayhelloのインスタンスを次のように置き換えます:
:( println("Hello, world!") )@sayhelloがREPLに入力されると、式は即座に実行されるため、評価結果のみが表示されます:
julia> @sayhello()
Hello, world!少し複雑なマクロを考えてみましょう:
julia> macro sayhello(name)
return :( println("Hello, ", $name) )
end
@sayhello (macro with 1 method)このマクロは1つの引数を取ります: 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"))Hold up: why macros?
私たちはすでに前のセクションで 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)Macro invocation
マクロは以下の一般的な構文で呼び出されます:
@name expr1 expr2 ...
@name(expr1, expr2, ...)マクロ名の前にある区別のための @ と、最初の形式での引数式の間にカンマがないこと、そして2番目の形式での @name の後に空白がないことに注意してください。これらの2つのスタイルは混合してはいけません。例えば、以下の構文は上記の例とは異なります。これは、タプル (expr1, expr2, ...) をマクロへの1つの引数として渡します:
@name (expr1, expr2, ...)マクロを配列リテラル(または内包表記)に適用する別の方法は、括弧を使用せずに両者を並べることです。この場合、配列はマクロに供給される唯一の式になります。以下の構文は等価であり(@name [a b] * v とは異なります):
@name[a b] * v
@name([a b]) * vマクロは引数を式、リテラル、またはシンボルとして受け取ることを強調することが重要です。マクロ引数を探る一つの方法は、マクロ本体内で show 関数を呼び出すことです:
julia> macro showarg(x)
show(x)
# ... remainder of macro, returning an expression
end
@showarg (macro with 1 method)
julia> @showarg(a)
:a
julia> @showarg(1+1)
:(1 + 1)
julia> @showarg(println("Yo!"))
:(println("Yo!"))
julia> @showarg(1) # Numeric literal
1
julia> @showarg("Yo!") # String literal
"Yo!"
julia> @showarg("Yo! $("hello")") # String with interpolation is an Expr rather than a String
:("Yo! $("hello")")与えられた引数リストに加えて、すべてのマクロには __source__ と __module__ という名前の追加引数が渡されます。
引数 __source__ は、マクロ呼び出しからの @ 記号のパーサー位置に関する情報(LineNumberNode オブジェクトの形式で)を提供します。これにより、マクロはより良いエラー診断情報を含めることができ、一般的にログ記録、文字列パーサーマクロ、ドキュメントなどで使用されます。例えば、@__LINE__、@__FILE__、および @__DIR__ マクロを実装するために使用されます。
位置情報は __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 オブジェクトの形式で)を提供します。これにより、マクロは既存のバインディングなどのコンテキスト情報を検索したり、現在のモジュール内で自己反射を行うランタイム関数呼び出しに追加の引数として値を挿入したりすることができます。
Building an advanced macro
ここに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)) の値がアサーションメッセージスロットに挿入されます。このように構築された全体の式は、@assert マクロ呼び出しが発生する構文木に配置されます。実行時に、テスト式が真に評価されると、nothing が返されますが、テストが偽の場合は、偽であったアサートされた式を示すエラーが発生します。このように、条件の値のみが利用可能であり、それを計算した式をエラーメッセージに表示することは不可能であるため、これを関数として書くことはできないことに注意してください。
@assertの実際の定義はJulia Baseではより複雑です。これは、ユーザーが失敗した式を単に印刷するのではなく、オプションで独自のエラーメッセージを指定できるようにします。可変数の引数を持つ関数と同様に(Varargs Functions)、これは最後の引数の後に省略記号を指定することで示されます。
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は受け取る引数の数に応じて2つの動作モードを持っています!引数が1つだけの場合、msgsによってキャプチャされた式のタプルは空になり、上記の簡単な定義と同じように動作します。しかし、ユーザーが2番目の引数を指定すると、失敗した式の代わりにメッセージ本文に印刷されます。マクロ展開の結果を調べるには、適切に名付けられた@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)別のケースがあります。実際の @assert マクロが処理するのは、"a should equal b" を印刷するだけでなく、それらの値も印刷したい場合です。例えば、カスタムメッセージで文字列補間を使おうとするかもしれませんが、@assert a==b "a ($a) should equal b ($b)!" のように、上記のマクロでは期待通りに動作しません。なぜか分かりますか? string interpolation を思い出してください。補間された文字列は、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にプレーンな文字列を取得する代わりに、マクロは期待通りに表示するために評価する必要がある完全な式を受け取ります。これは、string呼び出しの引数として返された式に直接スプライスできます。完全な実装については、error.jlを参照してください。
@assert マクロは、マクロ本体内の式の操作を簡素化するために、引用された式へのスプライシングを大いに活用しています。
Hygiene
より複雑なマクロで発生する問題の一つは、hygieneです。要するに、マクロは、返される式に導入する変数が、展開される周囲のコードに既存の変数と偶然に衝突しないようにする必要があります。逆に、マクロに引数として渡される式は、周囲のコードの文脈で評価されることが期待されることが多く、既存の変数と相互作用し、変更します。別のモジュールで定義されたマクロが呼び出される場合、別の懸念が生じます。この場合、すべてのグローバル変数が正しいモジュールに解決されることを確認する必要があります。Juliaは、テキストマクロ展開(Cのような)を持つ言語に対して、返される式だけを考慮すればよいという大きな利点があります。他のすべての変数(上記の@assertのmsgなど)は、normal scoping block behaviorに従います。
これらの問題を示すために、式を引数として受け取り、時間を記録し、式を評価し、再度時間を記録し、前後の時間の差を出力し、最後に式の値を最終値として持つ@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 がユーザーが持っているかもしれない time_ns 変数ではなく、Julia Base の time_ns 関数を参照するようにしたいと思います(println にも同様が適用されます)。ユーザーの式 ex に t0 という変数への代入が含まれていたり、独自の time_ns 変数が定義されていた場合に発生する可能性のある問題を想像してみてください。エラーが発生したり、神秘的に不正確な動作が起こるかもしれません。
Juliaのマクロエクスパンダーは、これらの問題を次のように解決します。まず、マクロの結果内の変数は、ローカルまたはグローバルのいずれかに分類されます。変数は、代入されている(かつグローバルとして宣言されていない)、ローカルとして宣言されている、または関数の引数名として使用されている場合、ローカルと見なされます。それ以外の場合は、グローバルと見なされます。ローカル変数は、その後、一意になるように名前が変更されます(新しいシンボルを生成するgensym関数を使用)、グローバル変数はマクロ定義環境内で解決されます。したがって、上記の2つの懸念はどちらも対処されます。マクロのローカルはユーザー変数と衝突せず、time_nsとprintlnはJulia Baseの定義を参照します。
しかし、1つの問題が残っています。このマクロの次の使用を考えてみてください:
module MyModule
import Base.@time
time_ns() = ... # compute something
@time time_ns()
endここでユーザーの式 ex は time_ns への呼び出しですが、マクロが使用する同じ time_ns 関数ではありません。これは明らかに MyModule.time_ns を指しています。したがって、ex のコードがマクロ呼び出し環境で解決されるように手配する必要があります。これは、式を esc で「エスケープ」することによって行われます。
macro time(ex)
...
local val = $(esc(ex))
...
endこのようにラップされた式は、マクロ展開器によってそのまま放置され、出力にそのまま貼り付けられます。したがって、それはマクロ呼び出し環境で解決されます。
このエスケープ機構は、必要に応じて「衛生」を「侵害」するために使用でき、ユーザー変数を導入または操作することができます。たとえば、次のマクロは呼び出し環境で x をゼロに設定します:
julia> macro zerox()
return esc(:(x = 0))
end
@zerox (macro with 1 method)
julia> function foo()
x = 1
@zerox
return x # is zero
end
foo (generic function with 1 method)
julia> foo()
0この種の変数の操作は慎重に使用すべきですが、時には非常に便利です。
ハイジーンルールを正しく理解することは、非常に困難な課題です。マクロを使用する前に、関数クロージャで十分かどうかを考慮することをお勧めします。もう一つの有用な戦略は、可能な限り多くの作業をランタイムに遅延させることです。例えば、多くのマクロは単にその引数を QuoteNode または他の類似の Expr にラップします。これには、単に schedule(Task(() -> $body)) を返す @task body や、単に eval(QuoteNode(expr)) を返す @eval 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がラップされたコードに最小限の影響で使用できることを望んでいます。
Macros and dispatch
マクロは、Juliaの関数と同様に、ジェネリックです。これは、複数のメソッド定義を持つことができることを意味し、これは多重ディスパッチのおかげです:
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 argumentsCode Generation
大量の繰り返しのボイラープレートコードが必要な場合、冗長性を避けるためにプログラム的に生成することが一般的です。ほとんどの言語では、これには追加のビルドステップと、繰り返しのコードを生成するための別のプログラムが必要です。Juliaでは、式の補間と eval により、そのようなコード生成がプログラムの実行の通常の過程で行われることができます。たとえば、次のカスタムタイプを考えてみましょう。
struct MyNumber
x::Float64
end
# output
追加したいメソッドの数に対して、以下のループでプログラム的に行うことができます:
for op = (:sin, :cos, :tan, :log, :exp)
eval(quote
Base.$op(a::MyNumber) = MyNumber($op(a.x))
end)
end
# output
そして、私たちは今、カスタムタイプでそれらの関数を使用できます:
julia> x = MyNumber(π)
MyNumber(3.141592653589793)
julia> sin(x)
MyNumber(1.2246467991473532e-16)
julia> cos(x)
MyNumber(-1.0)このように、Juliaは自らの preprocessor として機能し、言語内からのコード生成を可能にします。上記のコードは、: プレフィックスの引用形式を使用して、もう少し簡潔に書くことができます:
for op = (:sin, :cos, :tan, :log, :exp)
eval(:(Base.$op(a::MyNumber) = MyNumber($op(a.x))))
endこの種の言語内コード生成は、eval(quote(...))パターンを使用することで一般的であり、Juliaにはこのパターンを短縮するためのマクロが付属しています:
for op = (:sin, :cos, :tan, :log, :exp)
@eval Base.$op(a::MyNumber) = MyNumber($op(a.x))
end@eval マクロは、この呼び出しを上記の長いバージョンと正確に同等になるように書き換えます。生成されたコードの長いブロックの場合、4d61726b646f776e2e436f64652822222c2022406576616c2229_40726566 に与えられる式引数はブロックであることができます:
@eval begin
# multiple lines
endNon-Standard String Literals
Strings から思い出してください。識別子で接頭辞が付けられた文字列リテラルは非標準文字列リテラルと呼ばれ、接頭辞のない文字列リテラルとは異なる意味を持つことがあります。例えば:
r"^\s*(?:#|$)"は文字列ではなく、regular expression object を生成します。b"DATA\xff\u2200"は[68,65,84,65,255,226,136,128]の byte array literal です。
おそらく驚くべきことに、これらの動作はJuliaのパーサーやコンパイラーにハードコーディングされているわけではありません。代わりに、誰でも使用できる一般的なメカニズムによって提供されるカスタム動作です:接頭辞付き文字列リテラルは、特別に名前付けされたマクロへの呼び出しとして解析されます。たとえば、正規表現マクロは次のようになります:
macro r_str(p)
Regex(p)
endそれだけです。このマクロは、文字列リテラル r"^\s*(?:#|$)" のリテラル内容が @r_str マクロに渡され、その展開結果が文字列リテラルが発生する構文木に配置されるべきであることを示しています。言い換えれば、式 r"^\s*(?:#|$)" は、次のオブジェクトを構文木に直接配置することと同等です:
Regex("^\\s*(?:#|\$)")文字列リテラル形式は短く、はるかに便利であるだけでなく、効率も良いです。正規表現はコンパイルされ、Regexオブジェクトは実際にコードがコンパイルされるときに作成されるため、コンパイルはコードが実行されるたびではなく、一度だけ行われます。正規表現がループ内で発生する場合を考えてみてください:
for line = lines
m = match(r"^\s*(?:#|$)", line)
if m === nothing
# non-comment
else
# comment
end
end正規表現 r"^\s*(?:#|$)" はコンパイルされ、コードが解析されるときに構文木に挿入されるため、この式はループが実行されるたびにコンパイルされるのではなく、一度だけコンパイルされます。これをマクロなしで実現するためには、このループを次のように書く必要があります:
re = Regex("^\\s*(?:#|\$)")
for line = lines
m = match(re, line)
if m === nothing
# non-comment
else
# comment
end
endさらに、コンパイラが正規表現オブジェクトがすべてのループで定数であることを判断できない場合、特定の最適化が不可能になる可能性があり、このバージョンは上記のより便利なリテラル形式よりも依然として効率が悪くなるかもしれません。もちろん、非リテラル形式がより便利な状況もあります。変数を正規表現に埋め込む必要がある場合、このより冗長なアプローチを取らなければなりません。また、正規表現パターン自体が動的で、各ループの反復ごとに変更される可能性がある場合、各反復で新しい正規表現オブジェクトを構築する必要があります。しかし、ほとんどの使用ケースでは、正規表現は実行時データに基づいて構築されることはありません。この大多数のケースでは、正規表現をコンパイル時の値として記述する能力は非常に貴重です。
ユーザー定義の文字列リテラルのメカニズムは、非常に強力です。ジュリアの非標準リテラルはこれを使用して実装されているだけでなく、コマンドリテラル構文(`echo "Hello, $person"`)も次の無害に見えるマクロを使用して実装されています:
macro cmd(str)
:(cmd_gen($(shell_parse(str)[1])))
endもちろん、このマクロ定義で使用されている関数には多くの複雑さが隠れていますが、それらは単なる関数であり、完全にJuliaで書かれています。ソースを読むことができ、彼らが正確に何をしているのかを見ることができます。そして、彼らが行うことはすべて、あなたのプログラムの構文木に挿入される式オブジェクトを構築することだけです。
文字列リテラルと同様に、コマンドリテラルも識別子でプレフィックスを付けて、非標準コマンドリテラルと呼ばれるものを形成することができます。これらのコマンドリテラルは、特別に名前付けされたマクロへの呼び出しとして解析されます。例えば、構文 custom`literal` は @custom_cmd "literal" として解析されます。Julia自体には非標準コマンドリテラルは含まれていませんが、パッケージはこの構文を利用することができます。異なる構文と _str サフィックスの代わりに _cmd サフィックスがあることを除いて、非標準コマンドリテラルは非標準文字列リテラルとまったく同じように動作します。
2つのモジュールが同じ名前の非標準文字列またはコマンドリテラルを提供する場合、文字列またはコマンドリテラルをモジュール名で修飾することが可能です。たとえば、FooとBarの両方が非標準文字列リテラル@x_strを提供している場合、Foo.x"literal"またはBar.x"literal"と書くことで、2つの間の曖昧さを解消できます。
マクロを定義する別の方法は次のようになります:
macro foo_str(str, flag)
# do stuff
endこのマクロは次の構文で呼び出すことができます:
foo"str"flag上記の構文でのフラグのタイプは、文字列リテラルの後に続く内容を持つ String になります。
Generated functions
非常に特別なマクロは @generated で、いわゆる 生成関数 を定義することができます。これにより、引数の型に応じて特化したコードを生成する能力があり、複数のディスパッチを使用するよりも柔軟性があり、または少ないコードで実現できます。マクロはパース時に式で動作し、入力の型にアクセスできないのに対し、生成関数は引数の型が知られている時点で展開されますが、関数はまだコンパイルされていません。
計算やアクションを実行する代わりに、生成された関数宣言は引用された式を返し、それが引数の型に対応するメソッドの本体を形成します。生成された関数が呼び出されると、返された式がコンパイルされて実行されます。これを効率的にするために、結果は通常キャッシュされます。また、これを推論可能にするために、使用できる言語のサブセットは限られています。したがって、生成された関数は、許可される構造に対する制限が大きくなる代わりに、実行時からコンパイル時に作業を移動する柔軟な方法を提供します。
生成された関数を定義する際には、通常の関数との主な違いが5つあります:
- 関数宣言に
@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がどのように動作するか見てみましょう:
julia> x = foo(2); # note: output is from println() statement in the body
Int64
julia> x # now we print x
4
julia> y = foo("bar");
String
julia> y
"barbar"したがって、生成された関数の本体では、xは渡された引数の型であり、生成された関数が返す値は、定義から返された引用された式を、今度はxの値で評価した結果です。
fooをすでに使用した型で再評価するとどうなりますか?
julia> foo(4)
16Int64の印刷は行われないことに注意してください。生成された関数の本体は、特定の引数の型のセットに対してここで一度だけ実行されたことがわかります。そして、結果はキャッシュされました。その後、この例では、最初の呼び出しで生成された関数から返された式がメソッド本体として再利用されました。ただし、実際のキャッシュ動作は実装依存のパフォーマンス最適化であるため、この動作に過度に依存することは無効です。
生成された関数が生成される回数は1回だけかもしれませんが、それ以上の回数で生成されることもあるし、全く生成されないように見えることもあります。その結果、副作用のある生成された関数を書くべきではありません - 副作用が発生するタイミングや頻度は未定義です。(これはマクロにも当てはまります - そしてマクロと同様に、生成された関数内での eval の使用は、あなたが間違った方法で何かをしているサインです。)しかし、マクロとは異なり、ランタイムシステムは 4d61726b646f776e2e436f64652822222c20226576616c2229_40726566 への呼び出しを正しく処理できないため、それは許可されていません。
@generated 関数がメソッドの再定義とどのように相互作用するかを見ることも重要です。正しい @generated 関数は、可変状態を観察したり、グローバル状態を変更したりしてはいけないという原則に従うと、次のような動作が見られます。生成された関数は、生成された関数自体の 定義 の前に定義されていないメソッドを 呼び出すことができない ことに注意してください。
最初に 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コードを実行している間はロックを保持したり取得したりしないでください。生成された関数の本体の後に定義された関数を呼び出すこと。これは、モジュール内の任意の関数を呼び出すことを許可するために、インクリメンタルにロードされたプリコンパイル済みモジュールに対して緩和されます。
さて、生成関数がどのように機能するかをよりよく理解したので、これを使ってもう少し高度な(かつ有効な)機能を構築してみましょう...
An advanced example
Juliaの基本ライブラリには、n次元配列への線形インデックスを計算するための内部sub2ind関数があります。これは、n個の多次元インデックスのセットに基づいています。言い換えれば、A[x,y,z,...]の代わりにA[i]を使用して配列Aにインデックスを付けるために使用できるインデックス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;
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;
julia> sub2ind_gen((3, 5), 1, 2)
4このコードは何を生成しますか?
本体を別の(通常の)関数に抽出するのは簡単な方法です:
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;
julia> @generated function sub2ind_gen(dims::NTuple{N}, I::Integer...) where N
return sub2ind_gen_impl(dims, I...)
end;
julia> sub2ind_gen((3, 5), 1, 2)
4sub2ind_gen_implを実行し、それが返す式を調べることができます:
julia> sub2ind_gen_impl(Tuple{Int,Int}, Int, Int)
:(((I[1] - 1) + dims[1] * (I[2] - 1)) + 1)ここで使用されるメソッド本体にはループがまったく含まれていません - 2つのタプルへのインデックス付け、乗算、加算/減算のみです。すべてのループ処理はコンパイル時に行われ、実行中のループは完全に回避されます。したがって、タイプごとに1回、この場合はNごとに1回ループします(関数が複数回生成されるエッジケースを除く - 上記の免責事項を参照)。
Optionally-generated functions
生成された関数は実行時に高い効率を達成できますが、コンパイル時のコストが伴います:具体的な引数の型のすべての組み合わせに対して新しい関数本体を生成する必要があります。通常、Juliaは任意の引数に対して機能する「汎用」バージョンの関数をコンパイルすることができますが、生成された関数ではこれは不可能です。これは、生成された関数を多用するプログラムが静的にコンパイルすることが不可能である可能性があることを意味します。
この問題を解決するために、言語は生成された関数の通常の非生成代替実装を書くための構文を提供します。上記の sub2ind の例に適用すると、次のようになります:
julia> function sub2ind_gen_impl(dims::Type{T}, I...) where T <: NTuple{N,Any} where N
ex = :(I[$N] - 1)
for i = (N - 1):-1:1
ex = :(I[$i] - 1 + dims[$i] * $ex)
end
return :($ex + 1)
end;
julia> function sub2ind_gen_fallback(dims::NTuple{N}, I) where N
ind = I[N] - 1
for i = (N - 1):-1:1
ind = I[i] - 1 + dims[i]*ind
end
return ind + 1
end;
julia> function sub2ind_gen(dims::NTuple{N}, I::Integer...) where N
length(I) == N || error("partial indexing is unsupported")
if @generated
return sub2ind_gen_impl(dims, I...)
else
return sub2ind_gen_fallback(dims, I)
end
end;
julia> sub2ind_gen((3, 5), 1, 2)
4内部的に、このコードは関数の2つの実装を作成します。1つは if @generated の最初のブロックが使用される生成されたもので、もう1つは else ブロックが使用される通常のものです。 if @generated ブロックの then 部分の中では、コードは他の生成された関数と同じ意味を持ちます:引数名は型を参照し、コードは式を返す必要があります。複数の if @generated ブロックが存在する場合、生成された実装はすべての then ブロックを使用し、代替実装はすべての else ブロックを使用します。
関数の先頭にエラーチェックを追加したことに注意してください。このコードは両方のバージョンで共通であり、両方のバージョンで実行時コードです(生成されたバージョンから式として引用され、返されます)。つまり、ローカル変数の値と型はコード生成時には利用できません。コード生成コードは引数の型のみを見ることができます。
この定義スタイルでは、コード生成機能は本質的にオプションの最適化です。コンパイラは便利な場合にそれを使用しますが、そうでない場合は通常の実装を使用することを選択するかもしれません。このスタイルは好まれます。なぜなら、コンパイラがより多くの決定を下し、プログラムをより多くの方法でコンパイルできるようにし、通常のコードはコード生成コードよりも読みやすいためです。しかし、どの実装が使用されるかはコンパイラの実装の詳細に依存するため、2つの実装が同一に動作することが不可欠です。