宏¶
宏是接收编译时 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 代码。为了使其有效,我们必须使用 begin
和 end
将 case
包含在宏中
{% begin %}
case 42
{% for klass in [Int32, String] %}
when {{klass.id}}
p "is {{klass}}"
{% end %}
end
{% end %}