跳至内容

宏是接收编译时 AST 节点并在程序中粘贴代码的方法。例如

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

# This generates:
#
#     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 节点,如上面的示例所示。

请注意,节点按原样粘贴。如果在前面的示例中我们传递一个符号,生成的代码将变得无效

# 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 节点都是通过手动传递的参数、硬编码的值或从 `type` 或 `method` 信息帮助变量中检索到的。但是,在某些情况下,节点可能无法直接访问,例如,如果您使用来自不同上下文的的信息来构建到所需类型/常量的路径。

在这种情况下,`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"

顶层模块

可以使用一个特殊的变量访问顶层命名空间,作为 `TypeNode`:`@top_level`。以下示例展示了它的用途

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`

注意

目前,返回的数组将始终只有一个元素。

示例

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`

嵌套宏

可以定义一个生成一个或多个宏定义的宏。您必须在内部宏的宏表达式之前加上反斜杠字符 "\" 以转义它们,以防止它们被外部宏评估。

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 调用。使用它,您将无法使用任何变量插值,但无需转义内部宏字符。

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

上面的表达式将不会产生任何输出。

合并扩展和调用注释

The @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 语句的一个或多个 when 表达式,除非 case 是生成代码的一部分。

以下是一个无效宏的示例

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 不在宏中。宏生成的代码仅包含两个 when 表达式,它们本身不是有效的 Crystal 代码。为了使其有效,我们必须使用 beginendcase 包含在宏中

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