Makros sind Methoden, die AST-Knoten zur Kompilierzeit empfangen und Code erzeugen, der in ein Programm eingefügt wird. Ein Beispiel:

macro define_method(name, content)def{{name}}{{content}}endend# This generates:##     def foo#       1#     end
define_method foo,1

foo # => 1

Der Definitionskörper eines Makros sieht aus wie normaler Crystal-Code mit einer zusätzlichen Syntax zur Bearbeitung der AST-Knoten. Der erzeugte Code muss gültiger Crystal-Code sein, d.h. Sie können z.B. nicht einen def ohne einen passenden endoder ein einzelnes when Ausdruck einer case, da beides keine vollständigen gültigen Ausdrücke sind. Siehe Fallstricke für weitere Informationen.

Bereich

Makros, die auf der obersten Ebene deklariert sind, sind überall sichtbar. Wenn ein Makro der obersten Ebene markiert ist als private gekennzeichnet ist, ist es nur in dieser Datei zugänglich.

Sie können auch in Klassen und Modulen definiert werden und sind dann in diesen Bereichen sichtbar. Makros werden auch in der Kette der Vorfahren (Superklassen und eingeschlossene Module) nachgeschlagen.

Zum Beispiel kann ein Block, der ein Objekt als Standardempfänger erhält, indem er mit with ... yield aufgerufen wird, kann auf Makros zugreifen, die in der Vorgängerkette dieses Objekts definiert sind:

classFoomacro emphasize(value)"***#{{{value}} }***"enddefyield_with_selfwithselfyieldendendFoo.new.yield_with_self { emphasize(10)}# => "***10***"

Makros, die in Klassen und Modulen definiert sind, können auch von außerhalb dieser Klassen und Module aufgerufen werden:

classFoomacro emphasize(value)"***#{{{value}} }***"endendFoo.emphasize(10)# => "***10***"

Interpolation

Sie verwenden {{...}} um einen AST-Knoten einzufügen oder zu interpolieren, wie im obigen Beispiel.

Beachten Sie, dass der Knoten so eingefügt wird, wie er ist. Wenn wir im vorherigen Beispiel ein Symbol übergeben, wird der generierte Code ungültig:

# This generates:##     def :foo#       1#     end
define_method :foo,1

Beachten Sie, dass :foo das Ergebnis der Interpolation war, weil es dem Makro übergeben wurde. Sie können die Methode ASTNode#id verwenden, wenn Sie nur einen Bezeichner benötigen.

Makro-Aufrufe

Sie können ein feste Teilmenge von Methoden auf AST-Knoten zur Kompilierzeit aufrufen. Diese Methoden sind in einem fiktiven Crystal::Makros Modul dokumentiert.

Zum Beispiel kann der Aufruf von ASTNode#id im obigen Beispiel löst das Problem:

macro define_method(name, content)def{{name.id}}{{content}}endend# This correctly generates:##     def foo#       1#     end
define_method :foo,1

Module und Klassen

Es können auch Module, Klassen und Structs erzeugt werden:

macro define_class(module_name, class_name, method, content)module{{module_name}}class{{class_name}}definitialize(@name:String)enddef{{method}}{{content}}+@nameendendendend# 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"

Konditionale

Sie verwenden {% if condition %} ... {% end %} um bedingten Code zu erzeugen:

macro define_method(name, content)def{{name}}{%if content ==1%}"one"{%elsif content ==2%}"two"{%else%}{{content}}{%end%}endend

define_method foo,1
define_method bar,2
define_method baz,3

foo # => one
bar # => two
baz # => 3

Ähnlich wie bei regulärem Code, Nop, NilLiteral und ein falsches BoolLiteral werden als falsch, während alles andere als wahrhaftig.

Makrokonditionale können außerhalb einer Makrodefinition verwendet werden:

{%if env("TEST")%}
  puts "We are in test mode"{%end%}

Iteration

Man kann eine endliche Anzahl von Iterationen durchführen:

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...

Um eine Iteration 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

Die index Variable in dem obigen Beispiel ist optional.

Zur Iteration einer 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

Makro-Iterationen können außerhalb einer Makrodefinition verwendet werden:

{% for name, index in ["foo", "bar", "baz"] %}
  def {{name.id}}
    {{index}}
  end
{% end %}

foo # => 0
bar # => 1
baz # => 2

Variadische Argumente und Splatting

Ein Makro kann variadische Argumente akzeptieren:

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

Die Argumente werden in einem TupleLiteral gepackt und an das Makro übergeben.

Zusätzlich kann mit * beim Interpolieren einer TupleLiteral die durch Kommas getrennten Elemente interpoliert:

macro println(*values)
  print {{*values}}, 'n'
end

println 1, 2, 3 # outputs 123n

Typ-Informationen

Wenn ein Makro aufgerufen wird, kann man mit einer speziellen Instanzvariablen auf den aktuellen Bereich bzw. Typ zugreifen: @type. Der Typ dieser Variablen ist TypeNode, wodurch Sie zur Kompilierzeit Zugriff auf Typinformationen haben.

Beachten Sie, dass @type immer die Instanz ist. Typ, auch wenn das Makro in einer Klassenmethode aufgerufen wird.

Zum Beispiel:

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"

Informationen zur Methode

Wenn ein Makro aufgerufen wird, kann man auf die Methode zugreifen, das Makro ist in mit einer speziellen Instanzvariablen: @def. Der Typ dieser Variablen ist Def es sei denn, das Makro befindet sich außerhalb einer Methode, in diesem Fall ist es NilLiteral.

Beispiel:

module Foo
  def Foo.boo(arg1, arg2)
    {% @def.receiver %} # => Foo
    {% @def.name %}     # => boo
    {% @def.args %}     # => [arg1, arg2]
  end
end

Foo.boo(0, 1)

Konstanten

Makros können auf Konstanten zugreifen. Zum Beispiel:

VALUES = [1, 2, 3]

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

Wenn die Konstante einen Typ angibt, erhält man ein TypeNode.

Verschachtelte Makros

Es ist möglich, ein Makro zu definieren, das eine oder mehrere Makrodefinitionen erzeugt. Die Makroausdrücke des inneren Makros müssen mit einem vorangestellten Backslash-Zeichen "" abgeschlossen werden, damit sie nicht vom äußeren Makro ausgewertet werden können.

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
#       {% if greeting == "hola" %}
#         "¡hola alice!"
#       {% else %}
#         "{{greeting.id}} alice"
#       {% end %}
#     end
#     macro greeting_for_bob
#       {% 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!"

wortwörtlich

Eine weitere Möglichkeit, ein verschachteltes Makro zu definieren, ist die Verwendung des speziellen verbatim Aufruf. In diesem Fall können Sie keine Variableninterpolation verwenden, müssen aber die inneren Makrozeichen nicht entschlüsseln.

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
#       {% name = "alice" %}
#       {% if greeting == "hola" %}
#         "¡hola alice!"
#       {% else %}
#         "{{greeting.id}} alice"
#       {% end %}
#     end
#     macro greeting_for_bob
#       {% name = "bob" %}
#       {% 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!"

Beachten Sie, dass die Variablen im inneren Makro nicht innerhalb des verbatim Blocks. Der Inhalt des Blocks wird "so wie er ist" übertragen, im Wesentlichen als Zeichenkette, bis er vom Compiler erneut untersucht wird.

Kommentare

Makroausdrücke werden sowohl in Kommentaren als auch in kompilierbaren Codeabschnitten ausgewertet. Dies kann verwendet werden, um relevante Dokumentation für Expansionen bereitzustellen:

{% for name, index in ["foo", "bar", "baz"] %}
  # Provides a placeholder {{name.id}} method. Always returns {{index}}.
  def {{name.id}}
    {{index}}
  end
{% end %}

Diese Auswertung gilt sowohl für Interpolationen als auch für Direktiven. Dies hat zur Folge, dass Makros nicht auskommentiert werden können.

macro a
  # {% if false %}
  puts 42
  # {% end %}
end

a

Der obige Ausdruck führt zu keiner Ausgabe.

Pitfalls

Beim Schreiben von Makros (insbesondere außerhalb einer Makrodefinition) ist es wichtig, daran zu denken, dass der vom Makro erzeugte Code selbst gültiger Crystal-Code sein muss, noch bevor er in den Code des Hauptprogramms eingebunden wird. Das bedeutet zum Beispiel, dass ein Makro nicht ein oder mehrere when Ausdrücke eines case Anweisung erzeugen, es sei denn case ein Teil des generierten Codes war.

Hier ist ein Beispiel für ein solches ungültiges Makro:

case 42
{% for klass in [Int32, String] %} # Syntax Error: unexpected token: {% (expecting when, else or end)
  when {{klass.id}}
    p "is {{klass}}"
{% end %}
end

Beachten Sie, dass case nicht innerhalb des Makros steht. Der von dem Makro erzeugte Code besteht lediglich aus zwei when Ausdrücken, die für sich genommen keinen gültigen Crystal-Code darstellen. Wir müssen Folgendes einschließen case in das Makro einfügen, um es gültig zu machen, indem wir begin und end:

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