コンテンツへスキップ

マクロ

マクロは、コンパイル時に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コードではありません。beginend を使用してマクロ内にcaseを含める必要があります。

{% begin %}
  case 42
  {% for klass in [Int32, String] %}
    when {{klass.id}}
      p "is {{klass}}"
  {% end %}
  end
{% end %}