メタプログラミング

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 オブジェクトは二つの部分からなります:

式は前置記法を使った直接的な構築も可能です:

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 を使っても出力できます。この関数が出力するのは与えられた式 ExprS-式 であり、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.headex.args を入力するか、上述の dump または [email protected] を使ってください。

同様の式は 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) のように動作し、$xx の値で置き換わります。そして $ を二度付けると eval(eval(:x)) と同様となり、$$x1 + 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 への代入を行っています。

evalExpr オブジェクトを評価しているだけであり、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 に適用します。ab の使われ方の違いに気を付けてください:

式に対する関数

上で簡単に触れた通り、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" と表示するのに加えて、ab の値も表示したいとしたら? 文字列の補間を使って @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_nsprintln は Julia Base の定義を参照します。

ただ問題は一つ残ります。このマクロを次のように使ったとします:

module MyModule
import Base.@time

time_ns() = ... # 何かを計算する

@time time_ns()
end

このときユーザーが与える式 extime_ns の呼び出しとなりますが、もちろんこの time_nsMyModule.time_ns を指しています。よって ex に含まれる変数についてはマクロを呼び出した側の環境で解決されるよう調整が必要です。この調整は esc で式を “エスケープ” すると行えます:

macro time(ex)
    ...
    local val = $(esc(ex))
    ...
end

このように esc で囲った式はマクロの展開処理でそのまま出力にペーストされるので、名前の解決はマクロの呼び出し側の環境で行われます。

必要な場合にはこの esc を使ったエスケープ処理を使ってマクロの健全性を破るような変数の生成・操作も可能です。例えば、次のマクロは呼び出し側の環境における変数 x0 に設定します:

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 bodyschedule(Task(() -> $body)) を返すだけであり、@eval expreval(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

非標準文字列リテラル

文字列の章で説明したように、先頭に識別子が付いた文字列リテラルは非標準文字列リテラルと呼ばれ、先頭に何も付いていない通常の文字列リテラルとは異なる意味論を持つことができます。いくつか例を示します:

驚くかもしれませんが、標準文字列リテラルの動作は 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 であることを除けば、非標準コマンドリテラルは非標準文字列リテラルと全く同じように動作します。

二つのモジュールが同じ名前の非標準の文字列リテラルまたはコマンドリテラルを提供しているときは、リテラルの先頭に付いている識別子にモジュールの名前を付けることができます。例えば FooBar の両方が非標準文字列リテラル @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) を生成する非常に特殊なマクロです。被生成関数を使うと、引数の型に基づいた柔軟なコード生成が多重ディスパッチよりも少ないコードで可能になります。マクロはパース時に式に対して処理を行うので入力の型にアクセスできませんが、被生成関数は引数の型が分かっているときに呼び出され、引数の型ごとに一度だけコンパイルされます。

被生成関数は計算や処理を行うのではなく、引数の型に対応するメソッドの本体を表すクオートされた式を返します。被生成関数を呼び出すと、返り値の式がコンパイルされて実行されます。被生成関数では効率を高めるために結果の式が通常キャッシュされます。また型の推論が行えるように、一部の言語機能しか使えません。つまり被生成関数は利用できる構文を犠牲にすることで実行時の処理をコンパイル時に移動させる柔軟な方法を提供します。

被生成関数の定義と通常の関数の定義の間には、五つの大きな違いがあります:

  1. 被生成関数の宣言には @generated という注釈を付けます。こうすると AST に情報が加わり、定義されるのが被生成関数であることがコンパイラに伝わります。
  2. 被生成関数の本体からアクセスできるのは引数のだけです ──引数の値にはアクセスできません。
  3. 被生成関数は何らかの値を計算したり何らかの処理を行ったりするのではなく、評価すると行うべき計算や処理が行われるクオートされた式を返します。
  4. 被生成関数は自身の定義より前に定義された関数だけを呼び出せます。自身の定義より後に定義された関数を呼び出すと、未来の世界時を持つ関数を呼び出しているという MethodErrors が発生します。
  5. 被生成関数は定数でないグローバルな状態を観測改変できません (例えば 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)

この被生成関数の本体は非決定的なので、その動作 (およびこの関数を呼び出すコードの動作) は未定義となります。

ここまでの例をコピーしないこと!

被生成関数がどう定義されどう呼ばれるかをこの節の例で理解できたことを願います。ただし、このような被生成関数は書かないでください。理由は次の通りです:

被生成関数で行うべきでない処理は無数にあるのに対して、現在のランタイムシステムが検出できる無効な処理は限られていることに注意してください。実行するとランタイムシステムが警告を出さずに壊れる操作も多くあり、そういった操作はたいてい定義からは明らかでない細かな部分に隠れています。関数ジェネレータは型推論時に実行されるので、被生成関数はコードの制限を遵守しなければなりません。

被生成関数で行うべきでない処理の例を示します:

  1. ネイティブポインタをキャッシュする。

  2. 何らかの形で Core.Compiler のメソッドに触れる。

  3. 可変な状態を観測する。

    • 被生成関数に対する推論は任意のタイミングで実行されるので、コードが可変な状態を観測・改変している途中に実行される可能性があります。
  4. ロックを取得する: 呼び出した C コードがロックを使うのは許されます (例えば malloc は内部でロックを必要としますが、これを呼び出すのは構いません)。しかし実行される Julia コードからロックを取得・保持しないでください。

  5. 被生成関数の本体より後に定義された関数を呼ぶことは許されません。漸進的に読み込まれる事前コンパイル済みモジュールではこの制限は緩められ、被生成関数はモジュール内の任意の関数を呼び出せます。

被生成関数の動作について理解が深まったと思うので、次は高度な機能を作るのに被生成関数を (正しく) 使ってみましょう...。

高度な例

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 ブロックが使われるかのどちらかとなります。

エラーチェックを関数の最初に追加していることに注目してください。このコードは二つのバージョンで共通であり、両方で実行時に実行されます (被生成バージョンではクオートされた上で関数で生成された式として返ります)。これはコード生成時にはローカル変数の型と値が利用できないことを意味します ──コードを生成するコードが見れるのは引数の型だけです。

この形式で関数を定義すると、コード生成の機能は本質的に省略可能な最適化として扱われます。コンパイラは気が向けばコード生成を使うかもしれませんが、そうでなければ通常の実装を使うかもしれません。この形式を使えばコンパイラがプログラムをコンパイルするときの選択肢が増え、さらにコード生成のコードがより読みやすくなります。そのためこの形式が推奨されますが、どちらの実装を使うかはコンパイラの実装に任されます。よって二つの実装が同じ動作をすることは非常に重要です。

日本語 Julia 書籍 (Amazon アソシエイト)
1 から始める Julia プログラミング
Julia プログラミングクックブック―言語仕様からデータ分析、機械学習、数値計算まで
スタンフォード ベクトル・行列からはじめる最適化数学