Skip to content

Latest commit

 

History

History
505 lines (342 loc) · 22.2 KB

content.adoc

File metadata and controls

505 lines (342 loc) · 22.2 KB

マクロ

著者: at_grandpa

この章では、Crystal のマクロについて説明します。

マクロとは

Crystal のマクロは次のようなものです。

  • 「 Crystal のコードを書く」コード

  • コンパイルフェーズで実行され、 Crystal のコードに展開される

  • 全マクロが展開されたあとの Crystal コードが実際にコンパイルされる

これだけではイメージが湧きづらいので、マクロがどのようなものかを実際に見てみましょう。次のコードを見てください。

link:./examples/example_01.cr[role=include]
  1. macro を用いてマクロを定義します

  2. 定義したマクロを呼び出します

    1. 引数 my_method, "hoge" がマクロに渡されます

    2. 引数をもとに処理が行われ、呼び出し箇所に Crystal コードが展開されます

  3. Crystal コードに展開された後、通常のコンパイルが行われます

つまり、マクロ展開後は次のようになります。

link:./examples/example_01_expanded.cr[role=include]

単純なメソッド定義とメソッド呼び出しに展開されています。その後、実際のコンパイルが行われます。マクロのイメージが湧きましたでしょうか。

マクロの利点

マクロを利用することで、コードの重複を排除できます。次のコードを見てください。

link:./examples/duplication_01_expanded.cr[role=include]

典型的な getter メソッドです。 nameage が似たようなメソッドになっています。マクロでこの重複を除去しましょう。

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 ドキュメントの各マクロの説明には、そのマクロの定義へのリンクがあるので、興味のある方は確認してみてください。

def_equals

オブジェクトの同値性比較を行う == メソッドを定義します。同値性比較を行う場合、複数あるインスタンス変数の比較を行います。通常の場合、コードは次のようになります。

link:./examples/def_equals_01_before.cr[role=include]

このコードを、 def_equals を使って書くと次のようになります。

link:./examples/def_equals_01_macro.cr[role=include]

とてもすっきりしました。マクロがいかに強力かがわかります。

record

record は Struct を簡単に定義できるマクロです。通常、 Struct の定義は次のように行います。

link:./examples/record_01_before.cr[role=include]

このコードを、record を使って書くと次のようになります。

link:./examples/record_01_macro.cr[role=include]

1行で定義が書けてしまいました。record は、この他に

  • ブロックを渡すことでメソッドを定義できる

  • 初期値を与えることができる

  • 初期値から型推論できる

という機能もあります。気になる方は record のマニュアルを読んでみてください。

p!, pp!

ppp と同じく、引数として渡された値を標準出力に出力しますが、渡された式自身も表示してくれます。デバッグ時に重宝します。

link:./examples/pp_exp.cr[role=include]

 

いかがでしたでしょうか。いくつかのマクロを紹介しましたが、この他にも標準のマクロは存在します。興味のある方は探してみてください。今までのコードがずっとすっきりするはずです。

マクロの文法

マクロの文法は 公式マニュアル に記載されています。この章では公式マニュアルを基本とし、より詳しく解説していきます。

マクロのおさらい

マクロの基本的な使い方をおさらいしましょう。次のコードを見てください。この章の冒頭で出たコードです。

link:./examples/syntax/basic_syntax.cr[role=include]

上記の (1) の部分では、 macro を用いてマクロの定義を書いています。(2) の部分では、定義されたマクロの呼び出しを行っています。このコードを crystal run すると、次のような流れで処理されます。

  1. マクロ呼び出し時の引数が my_macro に渡される

  2. 引数展開や条件分岐等の処理をし、 Crystal コードが生成される

  3. 生成された Crystal コードを、マクロ呼び出し部分に展開する

  4. すべてのマクロを展開し終えたら、 Crystal コードのコンパイルをする

  5. Crystal コードのコンパイルが終わったら実行する

この流れを頭の中に入れつつ、次のステップに進みましょう。

マクロと抽象構文木

Crystal のコードはパーサによって、抽象構文木( Abstract Syntax Tree )にパースされます。抽象構文木を構成する木構造の各要素を AST node と言います。つまり、 Crystal のコードは各 AST node で構成されています。

ここでマクロに話を戻します。マクロは Crystal のコードを組み立てるものでした。言い換えると「マクロは AST node を操作して Crystal コードを組み立てるもの」ということになります。実際、マクロが引数として受け取るのは AST node です。そのことを確かめてみましょう。

AST node

マクロが受け取る AST node の型を見てみましょう。

link:./examples/syntax/ast_node.cr[role=include]

NumberLiteralArrayLiteral などが表示されました。これらの class は Crystal::Macros::NumberLiteralCrystal::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 でも同様に、クラスメソッドのような扱いになります。

class 内に定義した場合
link:./examples/syntax/macro_scope_class.cr[role=include]

「マクロが呼び出せない」という問題に陥った場合は、こちらの例を思い出してください。

if

マクロでの条件分岐は if を使います。次のコードを見てください。

link:./examples/syntax/if.cr[role=include]

if での true/false の扱いは次のようになっています。

  • false として扱われるもの

    • Nop

    • NilLiteral

    • BoolLiteralfalse

  • true として扱われるもの

    • 上記以外

また、 ifmacro の外でも利用できます。

link:./examples/syntax/if_outside.cr[role=include]

これでちょっとしたマクロを素早く書くことができます。

for

マクロでのループは for を使います。次のコードを見てください。

link:./examples/syntax/for.cr[role=include]

ArrayLiteral を渡すと for 文が回り、メソッドを定義します。この for は、 HashLiteral にも対応しています。

link:./examples/syntax/for_hash.cr[role=include]

forif と同様、 macro の外でも利用できます。

link:./examples/syntax/for_outside.cr[role=include]

可変長引数

通常の Crystal コードの感覚で可変長引数を扱うことができます。引数の定義に * を付けるだけです。受け取った引数は TupleLiteral になります。

link:./examples/syntax/variadic_arguments.cr[role=include]

splat 展開

* は、 ArrayLiteralTupleLiteral の splat 展開にも使用できます。また、 **HashLiteralNamedTupleLiteral の 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 が用意されています。これを使うと、コンパイル時の型情報にアクセスできます。実際どんなメソッドが存在しているかを見たほうがわかりやすいので、いくつかご紹介します。@typeCrystal::Macros::TypeNode クラスです。

TypeNode#instance_vars

型に定義されているインスタンス変数を返します。返り値は Crystal::Macros::MetaVar クラスの配列です。MetaVar クラスは、変数やインスタンス変数を表す型で、名前( MetaVar#name )と型( MetaVar#type )を持っています。

link:./examples/syntax/type_instance_vars.cr[role=include]

TypeNode#methods

型に定義されているメソッドの情報を返します。返り値は Crystal::Macros::Def クラスの配列です。Def クラスは、 def 文を表す型で、メソッド定義に関するさまざまな情報を持っています。例えば、 Def#args は引数の情報、 Def#return_type はメソッドの返り値の型を表します。

link:./examples/syntax/type_methods.cr[role=include]

これらの他にもメソッドはたくさんあります。私の調べた限りでは、組み合わせればやりたいことはできるという、必要最低限なメソッドはそろっていました。興味のある方はぜひ調べてみてください。

Hooks

一部の特別な名前を持ったマクロは hooks と呼ばれ、特定のタイミングでコンパイル時に実行されます。

Table 1. Hooks
マクロ 効果

macro inherited …​ end

サブクラスが定義されたときに実行されるマクロ

macro included …​ end

モジュールが include されたときに実行されるマクロ

macro extended …​ end

モジュールが extend されたときに実行されるマクロ

macro method_added …​ end

メソッドが追加されたときに実行されるマクロ

macro method_missing …​ end

呼び出そうとしたメソッドが定義されていない場合に実行されるマクロ

macro finished …​ end

インスタンス変数の型が決定したあとに呼び出されるマクロ

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 などがあります。

Fresh variables

マクロが展開されると、マクロ内で定義した変数もそのまま展開され、 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 のような変数に置き換わっています。このように、マクロの実行フェーズで変数名を置き換えています。

まとめ

以上で、マクロとはどういうものか、マクロの文法はどうなっているのかなどの説明を終わります。マクロのおおまかなイメージは湧きましたでしょうか。自分はライブラリを書く際にマクロを使いますが、やはり重複排除の効果はすごいと思います。また、 DSL の提供も比較的簡単にできるのではないでしょうか。マクロに慣れてくると、 Crystal 本来のコードを書くよりもマクロを書いている比率が多くなる印象が強いです。今回のこの章を読み、「マクロが読めるようになった」「マクロが書けるようになった」という方が一人でも増えて頂けると幸いです。