著者: at_grandpa
この章では、Crystal のマクロについて説明します。
Crystal のマクロは次のようなものです。
-
「 Crystal のコードを書く」コード
-
コンパイルフェーズで実行され、 Crystal のコードに展開される
-
全マクロが展開されたあとの Crystal コードが実際にコンパイルされる
これだけではイメージが湧きづらいので、マクロがどのようなものかを実際に見てみましょう。次のコードを見てください。
link:./examples/example_01.cr[role=include]
-
macro
を用いてマクロを定義します -
定義したマクロを呼び出します
-
引数
my_method
,"hoge"
がマクロに渡されます -
引数をもとに処理が行われ、呼び出し箇所に Crystal コードが展開されます
-
-
Crystal コードに展開された後、通常のコンパイルが行われます
つまり、マクロ展開後は次のようになります。
link:./examples/example_01_expanded.cr[role=include]
単純なメソッド定義とメソッド呼び出しに展開されています。その後、実際のコンパイルが行われます。マクロのイメージが湧きましたでしょうか。
マクロを利用することで、コードの重複を排除できます。次のコードを見てください。
link:./examples/duplication_01_expanded.cr[role=include]
典型的な getter メソッドです。 name
と age
が似たようなメソッドになっています。マクロでこの重複を除去しましょう。
link:./examples/duplication_01_macro.cr[role=include]
マクロを定義し、そのマクロを呼び出しました。一見、元のコードよりも複雑になったように見えます。しかし、今後インスタンス変数が増えたとしても、マクロの呼び出し引数にその名前を渡すだけでよくなります。重複を排除できました。
実は、今回のような getter
のマクロは、標準ですでに搭載されています。よって、上記のコードは次のように書くことができます。
link:./examples/duplication_01_getter.cr[role=include]
かなりすっきりしました。このように、マクロを利用することですっきりとしたコードを書くことができます。
重複の除去によって、マクロ呼び出しのコードはすっきりしました。しかし、マクロ定義のコードはどうしても複雑になってしまいます。マクロの理解に加え、展開後の Crystal コードも理解しなければならないからです。
Crystal のバージョン 0.20.4
以前は、マクロ展開後のコードを知るすべはありませんでした。唯一のヒントは、エラーメッセージだけでした。しかし、 Crystal のバージョン 0.20.5
から crystal tool expand
コマンドが追加されました。
$ crystal tool expand --help
Usage: crystal tool expand [options] [programfile] [--] [arguments]
Options:
-D FLAG, --define FLAG Define a compile-time flag
-c LOC, --cursor LOC Cursor location with LOC as path/to/file.cr:line:column
-f text|json, --format text|json Output format text (default) or json
--error-trace Show full error trace
-h, --help Show this message
--no-color Disable colored output
--prelude Use given file as prelude
-s, --stats Enable statistics output
-p, --progress Enable progress output
-t, --time Enable execution time output
--stdin-filename Source file name to be read from STDIN
--cursor
オプションでカーソル位置を指定すると、カーソル上のマクロを展開した結果を表示できます。先程の getter
で試してみましょう。
$ crystal tool expand --cursor /path/to/getter.cr:5:3 /path/to/getter.cr
1 expansion found
expansion 1:
getter(name, age)
# expand macro 'getter' (/path/to/crystal-lang/src/object.cr:230:3)
~> def name
@name
end
def age
@age
end
マクロが展開されました。意図していた定義です。このコマンドはエディタから実行できるようにすると便利です。設定方法は、エディタそれぞれの方法を参照してください。この crystal tool expand
のおかげで、マクロのデバッグが格段にしやすくなりました。マクロを記述する際はぜひ活用してみてください。
Crystal には、標準で搭載されているマクロがあります。便利なものが多いのでいくつかご紹介します。crystal tool expand
を用いれば内容を把握できます。また、公式の API ドキュメントの各マクロの説明には、そのマクロの定義へのリンクがあるので、興味のある方は確認してみてください。
オブジェクトの同値性比較を行う ==
メソッドを定義します。同値性比較を行う場合、複数あるインスタンス変数の比較を行います。通常の場合、コードは次のようになります。
link:./examples/def_equals_01_before.cr[role=include]
このコードを、 def_equals
を使って書くと次のようになります。
link:./examples/def_equals_01_macro.cr[role=include]
とてもすっきりしました。マクロがいかに強力かがわかります。
record
は Struct を簡単に定義できるマクロです。通常、 Struct の定義は次のように行います。
link:./examples/record_01_before.cr[role=include]
このコードを、record
を使って書くと次のようになります。
link:./examples/record_01_macro.cr[role=include]
1行で定義が書けてしまいました。record
は、この他に
-
ブロックを渡すことでメソッドを定義できる
-
初期値を与えることができる
-
初期値から型推論できる
という機能もあります。気になる方は record
のマニュアルを読んでみてください。
マクロの文法は 公式マニュアル に記載されています。この章では公式マニュアルを基本とし、より詳しく解説していきます。
マクロの基本的な使い方をおさらいしましょう。次のコードを見てください。この章の冒頭で出たコードです。
link:./examples/syntax/basic_syntax.cr[role=include]
上記の (1)
の部分では、 macro
を用いてマクロの定義を書いています。(2)
の部分では、定義されたマクロの呼び出しを行っています。このコードを crystal run
すると、次のような流れで処理されます。
-
マクロ呼び出し時の引数が
my_macro
に渡される -
引数展開や条件分岐等の処理をし、 Crystal コードが生成される
-
生成された Crystal コードを、マクロ呼び出し部分に展開する
-
すべてのマクロを展開し終えたら、 Crystal コードのコンパイルをする
-
Crystal コードのコンパイルが終わったら実行する
この流れを頭の中に入れつつ、次のステップに進みましょう。
Crystal のコードはパーサによって、抽象構文木( Abstract Syntax Tree )にパースされます。抽象構文木を構成する木構造の各要素を AST node と言います。つまり、 Crystal のコードは各 AST node で構成されています。
ここでマクロに話を戻します。マクロは Crystal のコードを組み立てるものでした。言い換えると「マクロは AST node を操作して Crystal コードを組み立てるもの」ということになります。実際、マクロが引数として受け取るのは AST node です。そのことを確かめてみましょう。
マクロが受け取る AST node の型を見てみましょう。
link:./examples/syntax/ast_node.cr[role=include]
NumberLiteral
や ArrayLiteral
などが表示されました。これらの class は Crystal::Macros::NumberLiteral
や Crystal::Macros::ArrayLiteral
として定義されています。そして、全 AST node は Crystal::Macros::ASTNode
を継承しています。 Crystal::Macros::ASTNode
の幾つかのメソッドを紹介します。
#line_number
は、 AST node が書かれている行数を返します。
link:./examples/syntax/ast_node_line_number.cr[role=include]
#stringify
は、 AST node の文字列表現を返します。
link:./examples/syntax/ast_node_stringify.cr[role=include]
このように、 Crystal::Macros::ASTNode
class には AST node を操作するためのメソッドが定義されています。そして、それらを継承している class ( Crystal::Macros::ArrayLiteral
など)は、 AST node のメソッドに加え、それぞれの便利なメソッドが定義されています。例えば、 Crystal::Macros::ArrayLiteral
には Array
に似たメソッドが定義されています。
link:./examples/syntax/ast_node_array_literal.cr[role=include]
通常の Crystal コードと似たような操作感で書くことができます。AST node を操作しているのか、 Crystal コードを操作しているのかをしっかりと意識してプログラミングしましょう。
次からは、実際の文法を具体的に見ていきましょう。
マクロにもスコープがあります。
トップレベルに定義した場合は、通常のメソッドと同じようにどこからでも呼び出せるようになります。
link:./examples/syntax/macro_scope_global.cr[role=include]
また、トップレベルにマクロを定義する際に private
修飾子を付けると、そのファイル内からのみ呼び出せるようになります。
private
を付けてトップレベルに定義した場合link:./examples/syntax/macro_scope_global_private.cr[role=include]
class 内にマクロを定義した場合は、インスタンスメソッドではなくクラスメソッドと似たような扱いになることに注意してください。 また、module や struct でも同様に、クラスメソッドのような扱いになります。
link:./examples/syntax/macro_scope_class.cr[role=include]
「マクロが呼び出せない」という問題に陥った場合は、こちらの例を思い出してください。
マクロでの条件分岐は if
を使います。次のコードを見てください。
link:./examples/syntax/if.cr[role=include]
if
での true/false の扱いは次のようになっています。
-
false
として扱われるもの-
Nop
-
NilLiteral
-
BoolLiteral
のfalse
-
-
true
として扱われるもの-
上記以外
-
また、 if
は macro
の外でも利用できます。
link:./examples/syntax/if_outside.cr[role=include]
これでちょっとしたマクロを素早く書くことができます。
マクロでのループは for
を使います。次のコードを見てください。
link:./examples/syntax/for.cr[role=include]
ArrayLiteral
を渡すと for 文が回り、メソッドを定義します。この for
は、 HashLiteral
にも対応しています。
link:./examples/syntax/for_hash.cr[role=include]
for
も if
と同様、 macro
の外でも利用できます。
link:./examples/syntax/for_outside.cr[role=include]
通常の Crystal コードの感覚で可変長引数を扱うことができます。引数の定義に *
を付けるだけです。受け取った引数は TupleLiteral
になります。
link:./examples/syntax/variadic_arguments.cr[role=include]
*
は、 ArrayLiteral
と TupleLiteral
の splat 展開にも使用できます。また、 **
は HashLiteral
と NamedTupleLiteral
の splat 展開に使用できます。次のコードを見てください。
link:./examples/syntax/splat.cr[role=include]
展開は、各要素をカンマで区切った形になります。 HashLiteral
もそのままカンマ区切りで出力されますが、使い所によっては上記のように Syntax error
になります。
マクロは定数にアクセスできます。次のコードを見てください。
link:./examples/syntax/constants.cr[role=include]
一見、マクロ以外の部分をマクロが参照しているので違和感があります。
Crystal は定数の再代入は認めていません。再代入がある場合は、 already initialized constant XXX
というエラーでコンパイルに失敗します。つまり、定数の値は不変なのでマクロ解析のフェーズでも扱えるというわけです。
ネストしたマクロも書くことができます。つまり、「マクロ定義を生成するマクロ」です。
ネストしたマクロは、外側から順に内側に向かって展開されます。その際、内側のマクロは外側のマクロで展開されないように \
でエスケープする必要があります。公式マニュアルの例がわかりやすいので引用します。次のコードを見てください。
link:./examples/syntax/nested_macros.cr[role=include]
外側のマクロで展開しない部分だけエスケープしていることに注目してください。特に、
"\{{greeting.id}} {{name.id}}"
の部分では、外側のマクロで {{name.id}}
の部分は展開されますが、 \{{greeting.id}}
の部分は展開されません。\{{greeting.id}}
の部分は内側のマクロで展開されます。Nested macros は、マクロの記述で重複が多い場合に有効です。しかし、可読性が損なわれやすいので注意が必要です。
マクロで生成するコードは、それ単体で Crystal のコードとして完結していなければなりません。言い替えれば、 生成されたコードを別のファイルに書き出して正しくパースされるようなコードでなければなりません。この制約は忘れてしまいがちなので気をつけましょう。次の例を見てください。
ret = ""
var = "pitfalls"
ret = case var
{% for klass in [Int32, String] %}
when {{ klass.id }} then "#{var} is {{ klass }}"
{% end %}
end
一見、マクロが展開されたら正しい Crystal のコードが生成されるように見えます。しかし、マクロで生成されるコードは when
から始まる部分だけなので、Crystal のコードとしては不完全で、エラーとなります。
この場合は、 {% begin %} … {% end %}
でコードを括りましょう。
link:./examples/syntax/pitfalls_begin_end.cr[role=include]
こうすることで、マクロが生成するコードが正しい Crystal のコードとなるため、コンパイルが通るようになります。陥りやすい間違いなので気をつけてください。
マクロには特別なインスタンス変数 @type
が用意されています。これを使うと、コンパイル時の型情報にアクセスできます。実際どんなメソッドが存在しているかを見たほうがわかりやすいので、いくつかご紹介します。@type
は Crystal::Macros::TypeNode
クラスです。
型に定義されているインスタンス変数を返します。返り値は Crystal::Macros::MetaVar
クラスの配列です。MetaVar
クラスは、変数やインスタンス変数を表す型で、名前( MetaVar#name
)と型( MetaVar#type
)を持っています。
link:./examples/syntax/type_instance_vars.cr[role=include]
型に定義されているメソッドの情報を返します。返り値は Crystal::Macros::Def
クラスの配列です。Def
クラスは、 def
文を表す型で、メソッド定義に関するさまざまな情報を持っています。例えば、 Def#args
は引数の情報、 Def#return_type
はメソッドの返り値の型を表します。
link:./examples/syntax/type_methods.cr[role=include]
これらの他にもメソッドはたくさんあります。私の調べた限りでは、組み合わせればやりたいことはできるという、必要最低限なメソッドはそろっていました。興味のある方はぜひ調べてみてください。
一部の特別な名前を持ったマクロは hooks と呼ばれ、特定のタイミングでコンパイル時に実行されます。
マクロ | 効果 |
---|---|
|
サブクラスが定義されたときに実行されるマクロ |
|
モジュールが include されたときに実行されるマクロ |
|
モジュールが extend されたときに実行されるマクロ |
|
メソッドが追加されたときに実行されるマクロ |
|
呼び出そうとしたメソッドが定義されていない場合に実行されるマクロ |
|
インスタンス変数の型が決定したあとに呼び出されるマクロ |
inherited
の例を見てみましょう。
link:./examples/syntax/hooks_inherited.cr[role=include]
継承した場合のみ実行されるので、 SuperClass
には #type_name
が存在していないことがわかります。
method_missing
の例も見てみましょう。
link:./examples/syntax/hooks_method_missing.cr[role=include]
method_missing
の引数は Crystal::Macros::Call
です。これはメソッドの呼び出しを表すクラスです。#args
や #receiver
などがあります。
マクロが展開されると、マクロ内で定義した変数もそのまま展開され、 Crystal コードとして解釈されます。次の例を見てください。
link:./examples/syntax/fresh_variables_example1.cr[role=include]
これは、ローカル変数を上書きして重複を排除する際には有効です。しかし、ライブラリで提供するマクロなどでは、意図しない形で上書きされてしまう可能性があります。そのため、 fresh variables という構文が用意されています。次の例を見てください。
link:./examples/syntax/fresh_variables_example2.cr[role=include]
%変数名
とすることで、そのマクロのコンテキスト内で唯一の変数として扱われます。仕組みは簡単です。上記のコードで crystal tool expand
をしてみましょう。
$ crystal tool expand -c /path/to/fresh_variables_example.cr:6:1 /path/to/fresh_variables_example.cr
1 expansion found
expansion 1:
dont_update_x
# expand macro 'dont_update_x' (/path/to/fresh_variables_example.cr:2:1)
~> __temp_20 = 1
puts(__temp_20)
__temp_20
のような変数に置き換わっています。このように、マクロの実行フェーズで変数名を置き換えています。