コンテンツにスキップ

マクロ

マクロとは、コンパイル時に AST ノードを受け取り、コードを生成してそれをプログラムに書き込むメソッドです。例をあげます。

macro define_method(name, content)
  def {{name}}
    {{content}}
  end
end

# これで以下が生成されます。
#
#     def foo
#       1
#     end
define_method foo, 1

foo # => 1

マクロ定義の本体はほぼ通常の Crystal コードですが、AST ノードを扱うための拡張されたシンタックスを利用します。生成されたコードは正しい 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 ノードを貼り付ける、もしくは埋め込むには {{...}} を使います。

ノードは「そのまま」貼り付けされることに注意してください。例えばもし、上記の例でシンボルを渡した場合には、生成されたコードは不正なものとなります。

# これで以下が生成されます:
#
#     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

# 以下が正しく生成される:
#
#     def foo
#       1
#     end
define_method :foo, 1

モジュールとクラス

モジュールやクラス、構造体を生成することもできます。

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

# これで以下が生成される:
#     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

通常のコードと同様に、NopNilLiteral そして偽の 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

The arguments are packed into a TupleLiteral and passed to the macro.

Additionally, using * when interpolating a TupleLiteral interpolates the elements separated by commas:

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"

The top level module

It is possible to access the top-level namespace, as a TypeNode, with a special variable: @top_level. The following example shows its utility:

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)

定数

マクロは定数にアクセスすることができます。例をあげます。

VALUES = [1, 2, 3]

{% for value in VALUES %}
  puts {{value}}
{% end %}

もし定数が型を示していれば、そのとき得られるのは TypeNode となります。

ネストしたマクロ

いくつかのマクロを生成するようなマクロを定義することができます。このとき、内側のマクロが外側のマクロによって評価されるのを防ぐため、バックラッシュでエスケープする必要があります。

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

ネストしたマクロを定義する他の方法としては、verbatim という特別なメソッドを使うものがあります。これを使うことで、内側のマクロをエスケープする必要がなくなります。

macro define_macros(*names)
  {% for name in names %}
    macro greeting_for_{{name.id}}(greeting)

      # name は verbatim ブロックの中では有効ではありません
      \{% 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

上記の式は何も出力しないでしょう。

落とし穴

マクロを書く際 (とくにマクロ定義の外で)、マクロによって生成されたコードは、メインのプログラムのコードにマージされる前から、それ自身として有効なコードである必要があることを覚えておいてください。要するに、例えば、マクロは冒頭の case が生成するコードに含まれていないようなcase 式の 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 のコードではありません。case をマクロによって生成されるコードに含めるために、beginend を使うことができます。

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