04. マクロ
この章では、Crystal のマクロについて説明します。
マクロとは
Crystal のマクロは次のようなものです。
-
「 Crystal のコードを書く」コード
-
コンパイルフェーズで実行され、 Crystal のコードに展開される
-
全マクロが展開されたあとの Crystal コードが実際にコンパイルされる
これだけではイメージが湧きづらいので、マクロがどのようなものかを実際に見てみましょう。次のコードを見てください。
macro my_macro(method_name, content)
def {{method_name}} (1)
{{content}}
end
end
my_macro(my_method, "hoge") (2)
my_method (3)
# => "hoge"
-
macro
を用いてマクロを定義します -
定義したマクロを呼び出します
-
引数
my_method
,"hoge"
がマクロに渡されます -
引数をもとに処理が行われ、呼び出し箇所に Crystal コードが展開されます
-
-
Crystal コードに展開された後、通常のコンパイルが行われます
つまり、マクロ展開後は次のようになります。
def my_method
"hoge"
end
my_method # => "hoge"
単純なメソッド定義とメソッド呼び出しに展開されています。その後、実際のコンパイルが行われます。マクロのイメージが湧きましたでしょうか。
マクロの利点
マクロを利用することで、コードの重複を排除できます。次のコードを見てください。
class User
def initialize(@name : String, @age : Int32)
end
def name
@name
end
def age
@age
end
end
user = User.new("Taro", 30)
user.name # => "Taro"
user.age # => 30
典型的な getter メソッドです。 name
と age
が似たようなメソッドになっています。マクロでこの重複を除去しましょう。
class User
def initialize(@name : String, @age : Int32)
end
# macroの定義
macro my_getter(*names)
{% for name in names %}
def {{name.id}}
@{{name.id}}
end
{% end %}
end
# macroの呼び出し
my_getter name, age
end
user = User.new("Taro", 30)
user.name # => "Taro"
user.age # => 30
マクロを定義し、そのマクロを呼び出しました。一見、元のコードよりも複雑になったように見えます。しかし、今後インスタンス変数が増えたとしても、マクロの呼び出し引数にその名前を渡すだけでよくなります。重複を排除できました。
実は、今回のような getter
のマクロは、標準ですでに搭載されています。よって、上記のコードは次のように書くことができます。
class User
def initialize(@name : String, @age : Int32)
end
getter name, age
end
user = User.new("Taro", 30)
user.name # => "Taro"
user.age # => 30
かなりすっきりしました。このように、マクロを利用することですっきりとしたコードを書くことができます。
マクロの展開
重複の除去によって、マクロ呼び出しのコードはすっきりしました。しかし、マクロ定義のコードはどうしても複雑になってしまいます。マクロの理解に加え、展開後の 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
オブジェクトの同値性比較を行う ==
メソッドを定義します。同値性比較を行う場合、複数あるインスタンス変数の比較を行います。通常の場合、コードは次のようになります。
struct User
def initialize(@name : String, @age : Int32)
end
# 同値性比較のメソッドを定義
def ==(other : self)
return false unless @name == other.@name
return false unless @age == other.@age
true
end
end
user1 = User.new "Taro", 30
user2 = User.new "Taro", 30
user1 == user2 # => true
このコードを、 def_equals
を使って書くと次のようになります。
struct User
def initialize(@name : String, @age : Int32)
end
# 同値性比較のメソッドを定義
def_equals @name, @age
end
user1 = User.new "Taro", 30
user2 = User.new "Taro", 30
user1 == user2 # => true
とてもすっきりしました。マクロがいかに強力かがわかります。
record
record
は Struct を簡単に定義できるマクロです。通常、 Struct の定義は次のように行います。
# Structの定義
struct User
property name : String
property age : Int32
def initialize(@name, @age)
end
end
user1 = User.new "Taro", 30
user1 # => User(@name="Taro", @age=30)
user1.name # => "Taro"
user1.age # => 30
このコードを、record
を使って書くと次のようになります。
# Structの定義
record User, name : String, age : Int32
user1 = User.new "Taro", 30
user1 # => User(@name="Taro", @age=30)
user1.name # => "Taro"
user1.age # => 30
1行で定義が書けてしまいました。record
は、この他に
-
ブロックを渡すことでメソッドを定義できる
-
初期値を与えることができる
-
初期値から型推論できる
という機能もあります。気になる方は record
のマニュアルを読んでみてください。
p!
, pp!
p
や pp
と同じく、引数として渡された値を標準出力に出力しますが、渡された式自身も表示してくれます。デバッグ時に重宝します。
arr = [1, 2, 3]
pp! arr.map(&.* 1000)
# output:
# arr.map(&.*(1000)) # => [1000, 2000, 3000]
いかがでしたでしょうか。いくつかのマクロを紹介しましたが、この他にも標準のマクロは存在します。興味のある方は探してみてください。今までのコードがずっとすっきりするはずです。
マクロの文法
マクロの文法は 公式マニュアル に記載されています。この章では公式マニュアルを基本とし、より詳しく解説していきます。
マクロのおさらい
マクロの基本的な使い方をおさらいしましょう。次のコードを見てください。この章の冒頭で出たコードです。
macro my_macro(method_name, content)
def {{method_name}} (1)
{{content}}
end
end
my_macro my_method, "hoge" (2)
my_method # => "hoge"
上記の (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
マクロが受け取る AST node の型を見てみましょう。
# 引数のclass_nameを表示させるmacro
macro ast_node_class_name(ast_node)
{{ ast_node.class_name }}
end
ast_node_class_name(1) # => "NumberLiteral"
ast_node_class_name("string") # => "StringLiteral"
ast_node_class_name(ast) # => "Call"
ast_node_class_name(["a", "b"]) # => "ArrayLiteral"
ast_node_class_name({key: "value"}) # => "NamedTupleLiteral"
NumberLiteral
や ArrayLiteral
などが表示されました。これらの class は Crystal::Macros::NumberLiteral
や Crystal::Macros::ArrayLiteral
として定義されています。そして、全 AST node は Crystal::Macros::ASTNode
を継承しています。 Crystal::Macros::ASTNode
の幾つかのメソッドを紹介します。
#line_number
は、 AST node が書かれている行数を返します。
macro ast_node_line_number(ast_node)
{{ ast_node.line_number }}
end
# ... 他のコードが並ぶ
ast_node_line_number 1 # => 12
ast_node_line_number "string" # => 13
#stringify
は、 AST node の文字列表現を返します。
macro ast_node_stringify(ast_node)
{{ ast_node.stringify }}
end
ast_node_stringify 1 # => "1"
ast_node_stringify "string" # => "\"string\""
ast_node_stringify ["a", "b"] # => "[\"a\", \"b\"]"
ast_node_stringify CONST # => "CONST"
このように、 Crystal::Macros::ASTNode
class には AST node を操作するためのメソッドが定義されています。そして、それらを継承している class ( Crystal::Macros::ArrayLiteral
など)は、 AST node のメソッドに加え、それぞれの便利なメソッドが定義されています。例えば、 Crystal::Macros::ArrayLiteral
には Array
に似たメソッドが定義されています。
macro ast_node_array_literal(ast_node)
{{ ast_node.map(&.capitalize).join("::") }}
end
ast_node_array_literal ["apple", "banana"] # => "Apple::Banana"
通常の Crystal コードと似たような操作感で書くことができます。AST node を操作しているのか、 Crystal コードを操作しているのかをしっかりと意識してプログラミングしましょう。
次からは、実際の文法を具体的に見ていきましょう。
スコープ
マクロにもスコープがあります。
トップレベルに定義した場合は、通常のメソッドと同じようにどこからでも呼び出せるようになります。
# トップレベルで定義されたマクロは、どこからでも参照可能
macro my_macro
"expanded my macro!!!"
end
my_macro # => "expanded my macro!!!"
また、トップレベルにマクロを定義する際に private
修飾子を付けると、そのファイル内からのみ呼び出せるようになります。
private
を付けてトップレベルに定義した場合# private を付けた場合は、同一ファイル内でのみ参照可能
private macro my_macro
"expanded my macro!!!"
end
my_macro # => "expanded my macro!!!"
class 内にマクロを定義した場合は、インスタンスメソッドではなくクラスメソッドと似たような扱いになることに注意してください。
また、module や struct でも同様に、クラスメソッドのような扱いになります。
class MacroScope
macro my_macro
"expanded my macro!!"
end
# class 内で参照可能
CONSTANT = my_macro
# インスタンスメソッド内で参照可能
def instance_method
my_macro
end
# クラスメソッド内で参照可能
def self.class_method
my_macro
end
end
# クラスメソッドと同じ syntax で参照可能
MacroScope.my_macro # => "expanded my macro!!"
# インスタンスメソッドとしては参照できない
# MacroScope.new.my_macro は undefined method になる
MacroScope::CONSTANT # => "expanded my macro!!"
MacroScope.new.instance_method # => "expanded my macro!!"
MacroScope.class_method # => "expanded my macro!!"
「マクロが呼び出せない」という問題に陥った場合は、こちらの例を思い出してください。
if
マクロでの条件分岐は if
を使います。次のコードを見てください。
macro conditionals(content)
{% if content == 1 %}
"one"
{% else %}
{{ content }}
{% end %}
end
conditionals 1 # => "one"
conditionals 2 # => 2
if
での true/false の扱いは次のようになっています。
-
false
として扱われるもの-
Nop
-
NilLiteral
-
BoolLiteral
のfalse
-
-
true
として扱われるもの-
上記以外
-
また、 if
は macro
の外でも利用できます。
{% if env("DEBUG") %}
puts "===== DEBUG MODE ======"
{% end %}
これでちょっとしたマクロを素早く書くことができます。
for
マクロでのループは for
を使います。次のコードを見てください。
macro iteration(names)
{% for name, index in names %}
def {{name}}
{{index}}
end
{% end %}
end
iteration [foo, bar, baz]
foo # => 0
bar # => 1
baz # => 2
ArrayLiteral
を渡すと for 文が回り、メソッドを定義します。この for
は、 HashLiteral
にも対応しています。
macro iteration(names)
{% for key, value in names %}
def {{key.id}}
{{value}}
end
{% end %}
end
iteration({foo: "FOO", bar: "BAR", baz: "BAZ"})
foo # => "FOO"
bar # => "BAR"
baz # => "BAZ"
for
も if
と同様、 macro
の外でも利用できます。
{% for name, index in ["foo", "bar", "baz"] %}
def {{name.id}}
{{index}}
end
{% end %}
foo # => 0
bar # => 1
baz # => 2
可変長引数
通常の Crystal コードの感覚で可変長引数を扱うことができます。引数の定義に *
を付けるだけです。受け取った引数は TupleLiteral
になります。
macro variadic_arguments(*names)
{% for name, index in names %}
def {{name.id}}
{{index}}
end
{% end %}
end
variadic_arguments foo, bar, baz
foo # => 0
bar # => 1
baz # => 2
splat 展開
*
は、 ArrayLiteral
と TupleLiteral
の splat 展開にも使用できます。また、 **
は HashLiteral
と NamedTupleLiteral
の splat 展開に使用できます。次のコードを見てください。
macro splat
p "Splatting a array"
{% array = [1, 2, 3] %}
p {{*array}}
p "Splatting a tuple"
{% tuple = {4, 5, 6} %}
p {{*tuple}}
# p "Double Splatting a hash"
# {% hash = {"a" => 1, "b" => 2} %}
# p {{**hash}}
# ------------
# Syntax error in expanded macro: splat:11: unexpected token: =>
#
# p "a" => 1, "b" => 2
# ^
p "Double Splatting a named tuple"
{% named_tuple = {"c": 3, "d": 4} %}
p {{**named_tuple}}
end
splat
# output:
# "Splatting a array"
# 1
# 2
# 3
# "Splatting a tuple"
# 4
# 5
# 6
# "Double Splatting a named tuple"
# {c: 3, d: 4}
展開は、各要素をカンマで区切った形になります。 HashLiteral
もそのままカンマ区切りで出力されますが、使い所によっては上記のように Syntax error
になります。
定数
マクロは定数にアクセスできます。次のコードを見てください。
CONSTANTS = ["foo", "bar", "baz"]
{% for value in CONSTANTS %}
puts {{value}}
{% end %}
# output:
# foo
# bar
# baz
一見、マクロ以外の部分をマクロが参照しているので違和感があります。
Crystal は定数の再代入は認めていません。再代入がある場合は、 already initialized constant XXX
というエラーでコンパイルに失敗します。つまり、定数の値は不変なのでマクロ解析のフェーズでも扱えるというわけです。
ネストしたマクロ
ネストしたマクロも書くことができます。つまり、「マクロ定義を生成するマクロ」です。
ネストしたマクロは、外側から順に内側に向かって展開されます。その際、内側のマクロは外側のマクロで展開されないように \
でエスケープする必要があります。公式マニュアルの例がわかりやすいので引用します。次のコードを見てください。
macro define_macros(*names)
{% for name in names %}
macro greeting_for_{{name.id}}(greeting)
\{% if greeting == "hola" %}
"¡hola {{name.id}}!"
\{% else %}
"\{{greeting.id}} {{name.id}}"
\{% end %}
end
{% end %}
end
# This generates:
#
# macro greeting_for_alice(greeting)
# {% if greeting == "hola" %}
# "¡hola alice!"
# {% else %}
# "{{greeting.id}} alice"
# {% end %}
# end
# macro greeting_for_bob(greeting)
# {% if greeting == "hola" %}
# "¡hola bob!"
# {% else %}
# "{{greeting.id}} bob"
# {% end %}
# end
define_macros alice, bob
greeting_for_alice "hello" # => "hello alice"
greeting_for_bob "hallo" # => "hallo bob"
greeting_for_alice "hej" # => "hej alice"
greeting_for_bob "hola" # => "¡hola bob!"
外側のマクロで展開しない部分だけエスケープしていることに注目してください。特に、
"\{{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 %}
でコードを括りましょう。
ret = ""
{% begin %}
var = "pitfalls"
ret = case var
{% for klass in [Int32, String] %}
when {{ klass.id }} then "#{var} is {{ klass }}"
{% end %}
end
{% end %}
ret # => "pitfalls is String"
こうすることで、マクロが生成するコードが正しい Crystal のコードとなるため、コンパイルが通るようになります。陥りやすい間違いなので気をつけてください。
型の情報にアクセスできる @type
マクロには特別なインスタンス変数 @type
が用意されています。これを使うと、コンパイル時の型情報にアクセスできます。実際どんなメソッドが存在しているかを見たほうがわかりやすいので、いくつかご紹介します。@type
は Crystal::Macros::TypeNode
クラスです。
TypeNode#instance_vars
型に定義されているインスタンス変数を返します。返り値は Crystal::Macros::MetaVar
クラスの配列です。MetaVar
クラスは、変数やインスタンス変数を表す型で、名前( MetaVar#name
)と型( MetaVar#type
)を持っています。
class MyClass
def initialize(@name : String, @age : Int32)
end
def instance_variables_name
# インスタンス変数の名前を配列で返す
{{ @type.instance_vars.map(&.name.stringify) }}
end
def instance_variables_type
# インスタンス変数の型を配列で返す
{{ @type.instance_vars.map(&.type) }}
end
end
my_class = MyClass.new("Taro", 30)
my_class.instance_variables_name # => ["name", "age"]
my_class.instance_variables_type # => [String, Int32]
TypeNode#methods
型に定義されているメソッドの情報を返します。返り値は Crystal::Macros::Def
クラスの配列です。Def
クラスは、 def
文を表す型で、メソッド定義に関するさまざまな情報を持っています。例えば、 Def#args
は引数の情報、 Def#return_type
はメソッドの返り値の型を表します。
class MyClass
def m1
end
protected def m2
end
private def m3
end
def methods_name
# 定義されているメソッドの名前を返す
{{ @type.methods.map(&.name.stringify) }}
end
def methods_visibility
# 定義されているメソッドのアクセス修飾子を返す
{{ @type.methods.map(&.visibility.stringify) }}
end
end
my_class = MyClass.new
my_class.methods_name
# => ["m1", "m2", "m3", "methods_name", "methods_visibility"]
my_class.methods_visibility
# => [":public", ":protected", ":private", ":public", ":public"]
これらの他にもメソッドはたくさんあります。私の調べた限りでは、組み合わせればやりたいことはできるという、必要最低限なメソッドはそろっていました。興味のある方はぜひ調べてみてください。
Hooks
一部の特別な名前を持ったマクロは hooks と呼ばれ、特定のタイミングでコンパイル時に実行されます。
マクロ | 効果 |
---|---|
|
サブクラスが定義されたときに実行されるマクロ |
|
モジュールが include されたときに実行されるマクロ |
|
モジュールが extend されたときに実行されるマクロ |
|
メソッドが追加されたときに実行されるマクロ |
|
呼び出そうとしたメソッドが定義されていない場合に実行されるマクロ |
|
インスタンス変数の型が決定したあとに呼び出されるマクロ |
inherited
の例を見てみましょう。
class SuperClass
macro inherited
def type_name
{{ @type.name.stringify }}
end
end
end
class SubClass < SuperClass
end
# SuperClass.new.type_name は undefined method 'type_name' for SuperClass となる
SubClass.new.type_name # => "SubClass"
継承した場合のみ実行されるので、 SuperClass
には #type_name
が存在していないことがわかります。
method_missing
の例も見てみましょう。
macro method_missing(call)
puts "name: {{call.name.id}}"
puts "args: {{call.args.size}} arguments"
end
foo
# output:
# name: foo
# args: 0 arguments
bar 1, 'a'
# output:
# name: bar
# args: 2 arguments
method_missing
の引数は Crystal::Macros::Call
です。これはメソッドの呼び出しを表すクラスです。#args
や #receiver
などがあります。
Fresh variables
マクロが展開されると、マクロ内で定義した変数もそのまま展開され、 Crystal コードとして解釈されます。次の例を見てください。
macro update_x
x = 1
end
x = 0
update_x
x # => 1
これは、ローカル変数を上書きして重複を排除する際には有効です。しかし、ライブラリで提供するマクロなどでは、意図しない形で上書きされてしまう可能性があります。そのため、 fresh variables という構文が用意されています。次の例を見てください。
macro dont_update_x
%x = 1
puts %x
end
x = 0
dont_update_x # outputs 1
x # => 0
%変数名
とすることで、そのマクロのコンテキスト内で唯一の変数として扱われます。仕組みは簡単です。上記のコードで 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 本来のコードを書くよりもマクロを書いている比率が多くなる印象が強いです。今回のこの章を読み、「マクロが読めるようになった」「マクロが書けるようになった」という方が一人でも増えて頂けると幸いです。