マクロ¶
マクロは、コンパイル時にASTノードを受け取り、プログラムに貼り付けられるコードを生成するメソッドです。例えば
macro define_method(name, content)
def {{name}}
{{content}}
end
end
# This generates:
#
# def foo
# 1
# end
define_method foo, 1
foo # => 1
マクロの定義本体は、ASTノードを操作するための追加構文を含む通常のCrystalコードのように見えます。生成されたコードは有効なCrystalコードでなければなりません。つまり、例えば対応する`end`のない`def`、または`case`の単一の`when`式を生成することはできません。どちらも完全な有効な式ではないためです。落とし穴を参照してください。
スコープ¶
トップレベルで宣言されたマクロはどこからでも見えます。トップレベルのマクロが`private`としてマークされている場合、そのファイル内でのみアクセスできます。
クラスやモジュール内で定義することもでき、それらのスコープ内で見えます。マクロは祖先チェーン(スーパークラスとインクルードされたモジュール)でも検索されます。
例えば、`with ... yield`で呼び出されることによってデフォルトのレシーバとして使用するオブジェクトが与えられるブロックは、そのオブジェクトの祖先チェーン内で定義されたマクロにアクセスできます。
class Foo
macro emphasize(value)
"***#{ {{value}} }***"
end
def yield_with_self(&)
with self yield
end
end
Foo.new.yield_with_self { emphasize(10) } # => "***10***"
クラスやモジュール内で定義されたマクロは、外部からも呼び出すことができます。
class Foo
macro emphasize(value)
"***#{ {{value}} }***"
end
end
Foo.emphasize(10) # => "***10***"
補間¶
上記の例のように、`{{...}}`を使用してASTノードを貼り付けたり、補間したりします。
ノードはそのまま貼り付けられることに注意してください。前の例でシンボルを渡すと、生成されたコードが無効になります。
# This generates:
#
# def :foo
# 1
# end
define_method :foo, 1
` :foo`が補間の結果であったことに注意してください。これはマクロに渡されたものだからです。識別子のみが必要な場合は、これらの場合にメソッド`ASTNode#id`を使用できます。
マクロ呼び出し¶
コンパイル時にASTノードに対して**固定されたサブセット**のメソッドを呼び出すことができます。これらのメソッドは架空のCrystal::Macrosモジュールで文書化されています。
例えば、上記の例で`ASTNode#id`を呼び出すことで、問題が解決します。
macro define_method(name, content)
def {{name.id}}
{{content}}
end
end
# This correctly generates:
#
# def foo
# 1
# end
define_method :foo, 1
parse_type¶
ほとんどのASTノードは、手動で渡された引数、ハードコードされた値、または型またはメソッド情報ヘルパー変数から取得されたいずれかによって取得されます。しかし、異なるコンテキストの情報を使用して目的の型/定数へのパスを構築する場合など、ノードに直接アクセスできない場合もあります。
このような場合、`parse_type`マクロメソッドは、提供された`StringLiteral`を、目的のASTノードに解決できるものに変換することで役立ちます。
MY_CONST = 1234
struct Some::Namespace::Foo; end
{{ parse_type("Some::Namespace::Foo").resolve.struct? }} # => true
{{ parse_type("MY_CONST").resolve }} # => 1234
{{ parse_type("MissingType").resolve }} # Error: undefined constant MissingType
詳細な例については、APIドキュメントを参照してください。
モジュールとクラス¶
モジュール、クラス、構造体も生成できます。
macro define_class(module_name, class_name, method, content)
module {{module_name}}
class {{class_name}}
def initialize(@name : String)
end
def {{method}}
{{content}} + @name
end
end
end
end
# This generates:
# module Foo
# class Bar
# def initialize(@name : String)
# end
#
# def say
# "hi " + @name
# end
# end
# end
define_class Foo, Bar, say, "hi "
p Foo::Bar.new("John").say # => "hi John"
条件分岐¶
`{% if condition %}` ... `{% end %}`を使用して、コードを条件付きで生成します。
macro define_method(name, content)
def {{name}}
{% if content == 1 %}
"one"
{% elsif content == 2 %}
"two"
{% else %}
{{content}}
{% end %}
end
end
define_method foo, 1
define_method bar, 2
define_method baz, 3
foo # => one
bar # => two
baz # => 3
通常のコードと同様に、`Nop`、`NilLiteral`、falseの`BoolLiteral`は偽と見なされ、それ以外は真と見なされます。
マクロの条件分岐は、マクロ定義の外で使用できます。
{% if env("TEST") %}
puts "We are in test mode"
{% end %}
反復処理¶
有限回数の反復処理を行うことができます。
macro define_constants(count)
{% for i in (1..count) %}
PI_{{i.id}} = Math::PI * {{i}}
{% end %}
end
define_constants(3)
PI_1 # => 3.14159...
PI_2 # => 6.28318...
PI_3 # => 9.42477...
`ArrayLiteral`を反復処理するには
macro define_dummy_methods(names)
{% for name, index in names %}
def {{name.id}}
{{index}}
end
{% end %}
end
define_dummy_methods [foo, bar, baz]
foo # => 0
bar # => 1
baz # => 2
上記の例での`index`変数は省略可能です。
`HashLiteral`を反復処理するには
macro define_dummy_methods(hash)
{% for key, value in hash %}
def {{key.id}}
{{value}}
end
{% end %}
end
define_dummy_methods({foo: 10, bar: 20})
foo # => 10
bar # => 20
マクロの反復処理は、マクロ定義の外で使用できます。
{% for name, index in ["foo", "bar", "baz"] %}
def {{name.id}}
{{index}}
end
{% end %}
foo # => 0
bar # => 1
baz # => 2
可変長引数とスプラット¶
マクロは可変長引数を受け入れることができます。
macro define_dummy_methods(*names)
{% for name, index in names %}
def {{name.id}}
{{index}}
end
{% end %}
end
define_dummy_methods foo, bar, baz
foo # => 0
bar # => 1
baz # => 2
引数は`TupleLiteral`にパックされ、マクロに渡されます。
さらに、`TupleLiteral`を補間する際に`*`を使用すると、コンマで区切られた要素が補間されます。
macro println(*values)
print {{*values}}, '\n'
end
println 1, 2, 3 # outputs 123\n
型情報¶
マクロが呼び出されると、特別なインスタンス変数`@type`を使用して、現在のスコープまたは型にアクセスできます。この変数の型は`TypeNode`であり、コンパイル時に型情報にアクセスできます。
マクロがクラスメソッドで呼び出された場合でも、`@type`は常にインスタンス型であることに注意してください。
例えば
macro add_describe_methods
def describe
"Class is: " + {{ @type.stringify }}
end
def self.describe
"Class is: " + {{ @type.stringify }}
end
end
class Foo
add_describe_methods
end
Foo.new.describe # => "Class is Foo"
Foo.describe # => "Class is Foo"
トップレベルモジュール¶
特別な変数`@top_level`を使用して、トップレベルの名前空間を`TypeNode`としてアクセスできます。次の例はその有用性を示しています。
A_CONSTANT = 0
{% if @top_level.has_constant?("A_CONSTANT") %}
puts "this is printed"
{% else %}
puts "this is not printed"
{% end %}
メソッド情報¶
マクロが呼び出されると、特別なインスタンス変数`@def`を使用して、マクロが存在するメソッドにアクセスできます。マクロがメソッドの外にある場合、この変数の型は`Def`になります。この場合、`NilLiteral`になります。
例
module Foo
def Foo.boo(arg1, arg2)
{% @def.receiver %} # => Foo
{% @def.name %} # => boo
{% @def.args %} # => [arg1, arg2]
end
end
Foo.boo(0, 1)
呼び出し情報¶
マクロが呼び出されると、特別なインスタンス変数`@caller`を使用して、マクロ呼び出しスタックにアクセスできます。この変数は、配列の最初の要素が最新の要素である`Call`ノードの`ArrayLiteral`を返します。マクロの外側、またはマクロに呼び出し元がない場合(例:フック)、値は`NilLiteral`です。
注記
現時点では、返される配列には常に1つの要素しかありません。
例
macro foo
{{ @caller.first.line_number }}
end
def bar
{{ @caller }}
end
foo # => 9
bar # => nil
定数¶
マクロは定数にアクセスできます。例えば
VALUES = [1, 2, 3]
{% for value in VALUES %}
puts {{value}}
{% end %}
定数が型を表す場合、`TypeNode`が返されます。
ネストされたマクロ¶
1つ以上のマクロ定義を生成するマクロを定義できます。外側のマクロによって評価されないようにするには、内側のマクロのマクロ式の前にバックスラッシュ文字「\」を付ける必要があります。
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!"
verbatim¶
ネストされたマクロを定義するもう1つの方法は、特別な`verbatim`呼び出しを使用することです。これを使用すると、変数の補間は使用できませんが、内側のマクロ文字をエスケープする必要はありません。
macro define_macros(*names)
{% for name in names %}
macro greeting_for_{{name.id}}(greeting)
# name will not be available within the verbatim block
\{% name = {{name.stringify}} %}
{% verbatim do %}
{% if greeting == "hola" %}
"¡hola {{name.id}}!"
{% else %}
"{{greeting.id}} {{name.id}}"
{% end %}
{% end %}
end
{% end %}
end
# This generates:
#
# macro greeting_for_alice(greeting)
# {% name = "alice" %}
# {% if greeting == "hola" %}
# "¡hola {{name.id}}!"
# {% else %}
# "{{greeting.id}} {{name.id}}"
# {% end %}
# end
# macro greeting_for_bob(greeting)
# {% name = "bob" %}
# {% if greeting == "hola" %}
# "¡hola {{name.id}}!"
# {% else %}
# "{{greeting.id}} {{name.id}}"
# {% 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!"
内部マクロ内の変数は、verbatim
ブロック内では使用できません。ブロックの内容は文字列として「そのまま」転送され、コンパイラによって再検査されるまで変更されません。
コメント¶
マクロ式は、コメント内とコードのコンパイル可能なセクションの両方で評価されます。これは、展開に関する関連ドキュメントを提供するために使用できます。
{% for name, index in ["foo", "bar", "baz"] %}
# Provides a placeholder {{name.id}} method. Always returns {{index}}.
def {{name.id}}
{{index}}
end
{% end %}
この評価は、補間とディレクティブの両方に適用されます。この結果、マクロをコメントアウトすることはできません。
macro a
# {% if false %}
puts 42
# {% end %}
end
a
上記の式は、出力されません。
展開コメントと呼び出しコメントのマージ¶
@caller
は #doc_comment
メソッドと組み合わせて使用することで、マクロによって生成されたノードのドキュメントコメントと、マクロ呼び出し自体のコメントをマージできます。例えば
macro gen_method(name)
# {{ @caller.first.doc_comment }}
#
# Comment added via macro expansion.
def {{name.id}}
end
end
# Comment on macro call.
gen_method foo
生成されると、#foo
メソッドのドキュメントは次のようになります。
Comment on macro call.
Comment added via macro expansion.
注意点¶
マクロを作成する際(特にマクロ定義の外側で)、マクロから生成されたコードは、メインプログラムのコードにマージされる前でも、それ自体で有効なCrystalコードでなければならないことを覚えておくことが重要です。つまり、例えば、マクロは、case
が生成されたコードの一部でない限り、case
文の1つ以上のwhen
式を生成できません。
このような無効なマクロの例を以下に示します。
case 42
{% for klass in [Int32, String] %} # Syntax Error: unexpected token: {% (expecting when, else or end)
when {{klass.id}}
p "is {{klass}}"
{% end %}
end
case
はマクロ内にありません。マクロによって生成されたコードは、2つのwhen
式のみで構成されており、それ自体は有効なCrystalコードではありません。begin
と end
を使用してマクロ内にcase
を含める必要があります。
{% begin %}
case 42
{% for klass in [Int32, String] %}
when {{klass.id}}
p "is {{klass}}"
{% end %}
end
{% end %}