跳至內容

巨集

巨集是在編譯時期接收 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 程式碼,這意味著您不能例如產生一個沒有匹配的 enddef,或是一個 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

與常規程式碼類似,NopNilLiteral 和 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

可變引數與 splatting

巨集可以接受可變引數

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_levelTypeNode 的形式存取頂層命名空間。以下範例展示了它的用途。

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,其中陣列中的第一個元素是最新的呼叫。在巨集之外,或如果巨集沒有呼叫者(例如 hook),則值為 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

定義巢狀巨集的另一種方法是使用特殊的 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 語句的一個或多個 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 不在巨集中。巨集產生的程式碼僅由兩個 when 表達式組成,它們本身不是有效的 Crystal 程式碼。我們必須在巨集中包含 case,才能透過使用 beginend 使其有效。

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