04. マクロ

この章では、Crystal のマクロについて説明します。

マクロとは

Crystal のマクロは次のようなものです。

  • 「 Crystal のコードを書く」コード

  • コンパイルフェーズで実行され、 Crystal のコードに展開される

  • 全マクロが展開されたあとの Crystal コードが実際にコンパイルされる

これだけではイメージが湧きづらいので、マクロがどのようなものかを実際に見てみましょう。次のコードを見てください。

macro my_macro(method_name, content)
  def {{method_name}} (1)
    {{content}}
  end
end

my_macro(my_method, "hoge") (2)

my_method (3)
# => "hoge"
  1. macro を用いてマクロを定義します

  2. 定義したマクロを呼び出します

    1. 引数 my_method, "hoge" がマクロに渡されます

    2. 引数をもとに処理が行われ、呼び出し箇所に Crystal コードが展開されます

  3. Crystal コードに展開された後、通常のコンパイルが行われます

つまり、マクロ展開後は次のようになります。

def my_method
  "hoge"
end

my_method # => "hoge"

単純なメソッド定義とメソッド呼び出しに展開されています。その後、実際のコンパイルが行われます。マクロのイメージが湧きましたでしょうか。

マクロの利点

マクロを利用することで、コードの重複を排除できます。次のコードを見てください。

class User
  def initialize(@name : String, @age : Int32)
  end

  def name
    @name
  end

  def age
    @age
  end
end

user = User.new("Taro", 30)
user.name # => "Taro"
user.age  # => 30

典型的な getter メソッドです。 nameage が似たようなメソッドになっています。マクロでこの重複を除去しましょう。

class User
  def initialize(@name : String, @age : Int32)
  end

  # macroの定義
  macro my_getter(*names)
    {% for name in names %}
      def {{name.id}}
        @{{name.id}}
      end
    {% end %}
  end

  # macroの呼び出し
  my_getter name, age
end

user = User.new("Taro", 30)
user.name # => "Taro"
user.age  # => 30

マクロを定義し、そのマクロを呼び出しました。一見、元のコードよりも複雑になったように見えます。しかし、今後インスタンス変数が増えたとしても、マクロの呼び出し引数にその名前を渡すだけでよくなります。重複を排除できました。

実は、今回のような getter のマクロは、標準ですでに搭載されています。よって、上記のコードは次のように書くことができます。

class User
  def initialize(@name : String, @age : Int32)
  end

  getter name, age
end

user = User.new("Taro", 30)
user.name # => "Taro"
user.age  # => 30

かなりすっきりしました。このように、マクロを利用することですっきりとしたコードを書くことができます。

マクロの展開

重複の除去によって、マクロ呼び出しのコードはすっきりしました。しかし、マクロ定義のコードはどうしても複雑になってしまいます。マクロの理解に加え、展開後の Crystal コードも理解しなければならないからです。

Crystal のバージョン 0.20.4 以前は、マクロ展開後のコードを知るすべはありませんでした。唯一のヒントは、エラーメッセージだけでした。しかし、 Crystal のバージョン 0.20.5 から crystal tool expand コマンドが追加されました。

$ crystal tool expand --help
Usage: crystal tool expand [options] [programfile] [--] [arguments]

Options:
    -D FLAG, --define FLAG           Define a compile-time flag
    -c LOC, --cursor LOC             Cursor location with LOC as path/to/file.cr:line:column
    -f text|json, --format text|json Output format text (default) or json
    --error-trace                    Show full error trace
    -h, --help                       Show this message
    --no-color                       Disable colored output
    --prelude                        Use given file as prelude
    -s, --stats                      Enable statistics output
    -p, --progress                   Enable progress output
    -t, --time                       Enable execution time output
    --stdin-filename                 Source file name to be read from STDIN

--cursor オプションでカーソル位置を指定すると、カーソル上のマクロを展開した結果を表示できます。先程の getter で試してみましょう。

$ crystal tool expand --cursor /path/to/getter.cr:5:3 /path/to/getter.cr
1 expansion found
expansion 1:
   getter(name, age)

# expand macro 'getter' (/path/to/crystal-lang/src/object.cr:230:3)
~> def name
     @name
   end
   def age
     @age
   end

マクロが展開されました。意図していた定義です。このコマンドはエディタから実行できるようにすると便利です。設定方法は、エディタそれぞれの方法を参照してください。この crystal tool expand のおかげで、マクロのデバッグが格段にしやすくなりました。マクロを記述する際はぜひ活用してみてください。

標準搭載のマクロ

Crystal には、標準で搭載されているマクロがあります。便利なものが多いのでいくつかご紹介します。crystal tool expand を用いれば内容を把握できます。また、公式の API ドキュメントの各マクロの説明には、そのマクロの定義へのリンクがあるので、興味のある方は確認してみてください。

def_equals

オブジェクトの同値性比較を行う == メソッドを定義します。同値性比較を行う場合、複数あるインスタンス変数の比較を行います。通常の場合、コードは次のようになります。

struct User
  def initialize(@name : String, @age : Int32)
  end

  # 同値性比較のメソッドを定義
  def ==(other : self)
    return false unless @name == other.@name
    return false unless @age == other.@age
    true
  end
end

user1 = User.new "Taro", 30
user2 = User.new "Taro", 30
user1 == user2 # => true

このコードを、 def_equals を使って書くと次のようになります。

struct User
  def initialize(@name : String, @age : Int32)
  end

  # 同値性比較のメソッドを定義
  def_equals @name, @age
end

user1 = User.new "Taro", 30
user2 = User.new "Taro", 30
user1 == user2 # => true

とてもすっきりしました。マクロがいかに強力かがわかります。

record

record は Struct を簡単に定義できるマクロです。通常、 Struct の定義は次のように行います。

# Structの定義
struct User
  property name : String
  property age : Int32

  def initialize(@name, @age)
  end
end

user1 = User.new "Taro", 30
user1      # => User(@name="Taro", @age=30)
user1.name # => "Taro"
user1.age  # => 30

このコードを、record を使って書くと次のようになります。

# Structの定義
record User, name : String, age : Int32

user1 = User.new "Taro", 30
user1      # => User(@name="Taro", @age=30)
user1.name # => "Taro"
user1.age  # => 30

1行で定義が書けてしまいました。record は、この他に

  • ブロックを渡すことでメソッドを定義できる

  • 初期値を与えることができる

  • 初期値から型推論できる

という機能もあります。気になる方は record のマニュアルを読んでみてください。

p!, pp!

ppp と同じく、引数として渡された値を標準出力に出力しますが、渡された式自身も表示してくれます。デバッグ時に重宝します。

arr = [1, 2, 3]

pp! arr.map(&.* 1000)
# output:
# arr.map(&.*(1000)) # => [1000, 2000, 3000]

 

いかがでしたでしょうか。いくつかのマクロを紹介しましたが、この他にも標準のマクロは存在します。興味のある方は探してみてください。今までのコードがずっとすっきりするはずです。

マクロの文法

マクロの文法は 公式マニュアル に記載されています。この章では公式マニュアルを基本とし、より詳しく解説していきます。

マクロのおさらい

マクロの基本的な使い方をおさらいしましょう。次のコードを見てください。この章の冒頭で出たコードです。

macro my_macro(method_name, content)
  def {{method_name}} (1)
    {{content}}
  end
end

my_macro my_method, "hoge" (2)
my_method # => "hoge"

上記の (1) の部分では、 macro を用いてマクロの定義を書いています。(2) の部分では、定義されたマクロの呼び出しを行っています。このコードを crystal run すると、次のような流れで処理されます。

  1. マクロ呼び出し時の引数が my_macro に渡される

  2. 引数展開や条件分岐等の処理をし、 Crystal コードが生成される

  3. 生成された Crystal コードを、マクロ呼び出し部分に展開する

  4. すべてのマクロを展開し終えたら、 Crystal コードのコンパイルをする

  5. Crystal コードのコンパイルが終わったら実行する

この流れを頭の中に入れつつ、次のステップに進みましょう。

マクロと抽象構文木

Crystal のコードはパーサによって、抽象構文木( Abstract Syntax Tree )にパースされます。抽象構文木を構成する木構造の各要素を AST node と言います。つまり、 Crystal のコードは各 AST node で構成されています。

ここでマクロに話を戻します。マクロは Crystal のコードを組み立てるものでした。言い換えると「マクロは AST node を操作して Crystal コードを組み立てるもの」ということになります。実際、マクロが引数として受け取るのは AST node です。そのことを確かめてみましょう。

AST node

マクロが受け取る AST node の型を見てみましょう。

# 引数のclass_nameを表示させるmacro
macro ast_node_class_name(ast_node)
  {{ ast_node.class_name }}
end

ast_node_class_name(1)              # => "NumberLiteral"
ast_node_class_name("string")       # => "StringLiteral"
ast_node_class_name(ast)            # => "Call"
ast_node_class_name(["a", "b"])     # => "ArrayLiteral"
ast_node_class_name({key: "value"}) # => "NamedTupleLiteral"

NumberLiteralArrayLiteral などが表示されました。これらの class は Crystal::Macros::NumberLiteralCrystal::Macros::ArrayLiteral として定義されています。そして、全 AST node は Crystal::Macros::ASTNode を継承しています。 Crystal::Macros::ASTNode の幾つかのメソッドを紹介します。

#line_number は、 AST node が書かれている行数を返します。

macro ast_node_line_number(ast_node)
  {{ ast_node.line_number }}
end

# ... 他のコードが並ぶ

ast_node_line_number 1        # => 12
ast_node_line_number "string" # => 13

#stringify は、 AST node の文字列表現を返します。

macro ast_node_stringify(ast_node)
  {{ ast_node.stringify }}
end

ast_node_stringify 1          # => "1"
ast_node_stringify "string"   # => "\"string\""
ast_node_stringify ["a", "b"] # => "[\"a\", \"b\"]"
ast_node_stringify CONST      # => "CONST"

このように、 Crystal::Macros::ASTNode class には AST node を操作するためのメソッドが定義されています。そして、それらを継承している class ( Crystal::Macros::ArrayLiteral など)は、 AST node のメソッドに加え、それぞれの便利なメソッドが定義されています。例えば、 Crystal::Macros::ArrayLiteral には Array に似たメソッドが定義されています。

macro ast_node_array_literal(ast_node)
  {{ ast_node.map(&.capitalize).join("::") }}
end

ast_node_array_literal ["apple", "banana"] # => "Apple::Banana"

通常の Crystal コードと似たような操作感で書くことができます。AST node を操作しているのか、 Crystal コードを操作しているのかをしっかりと意識してプログラミングしましょう。

次からは、実際の文法を具体的に見ていきましょう。

スコープ

マクロにもスコープがあります。

トップレベルに定義した場合は、通常のメソッドと同じようにどこからでも呼び出せるようになります。

トップレベルに定義した場合
# トップレベルで定義されたマクロは、どこからでも参照可能
macro my_macro
  "expanded my macro!!!"
end

my_macro # => "expanded my macro!!!"

また、トップレベルにマクロを定義する際に private 修飾子を付けると、そのファイル内からのみ呼び出せるようになります。

private を付けてトップレベルに定義した場合
# private を付けた場合は、同一ファイル内でのみ参照可能
private macro my_macro
  "expanded my macro!!!"
end

my_macro # => "expanded my macro!!!"

class 内にマクロを定義した場合は、インスタンスメソッドではなくクラスメソッドと似たような扱いになることに注意してください。
また、module や struct でも同様に、クラスメソッドのような扱いになります。

class 内に定義した場合
class MacroScope
  macro my_macro
    "expanded my macro!!"
  end

  # class 内で参照可能
  CONSTANT = my_macro

  # インスタンスメソッド内で参照可能
  def instance_method
    my_macro
  end

  # クラスメソッド内で参照可能
  def self.class_method
    my_macro
  end
end

# クラスメソッドと同じ syntax で参照可能
MacroScope.my_macro # => "expanded my macro!!"

# インスタンスメソッドとしては参照できない
# MacroScope.new.my_macro は undefined method になる

MacroScope::CONSTANT           # => "expanded my macro!!"
MacroScope.new.instance_method # => "expanded my macro!!"
MacroScope.class_method        # => "expanded my macro!!"

「マクロが呼び出せない」という問題に陥った場合は、こちらの例を思い出してください。

if

マクロでの条件分岐は if を使います。次のコードを見てください。

macro conditionals(content)
  {% if content == 1 %}
    "one"
  {% else %}
    {{ content }}
  {% end %}
end

conditionals 1 # => "one"
conditionals 2 # => 2

if での true/false の扱いは次のようになっています。

  • false として扱われるもの

    • Nop

    • NilLiteral

    • BoolLiteralfalse

  • true として扱われるもの

    • 上記以外

また、 ifmacro の外でも利用できます。

{% if env("DEBUG") %}
  puts "===== DEBUG MODE ======"
{% end %}

これでちょっとしたマクロを素早く書くことができます。

for

マクロでのループは for を使います。次のコードを見てください。

macro iteration(names)
  {% for name, index in names %}
    def {{name}}
      {{index}}
    end
  {% end %}
end

iteration [foo, bar, baz]

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

ArrayLiteral を渡すと for 文が回り、メソッドを定義します。この for は、 HashLiteral にも対応しています。

macro iteration(names)
  {% for key, value in names %}
    def {{key.id}}
      {{value}}
    end
  {% end %}
end

iteration({foo: "FOO", bar: "BAR", baz: "BAZ"})

foo # => "FOO"
bar # => "BAR"
baz # => "BAZ"

forif と同様、 macro の外でも利用できます。

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

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

可変長引数

通常の Crystal コードの感覚で可変長引数を扱うことができます。引数の定義に * を付けるだけです。受け取った引数は TupleLiteral になります。

macro variadic_arguments(*names)
  {% for name, index in names %}
    def {{name.id}}
      {{index}}
    end
  {% end %}
end

variadic_arguments foo, bar, baz

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

splat 展開

* は、 ArrayLiteralTupleLiteral の splat 展開にも使用できます。また、 **HashLiteralNamedTupleLiteral の splat 展開に使用できます。次のコードを見てください。

macro splat
  p "Splatting a array"
  {% array = [1, 2, 3] %}
  p {{*array}}

  p "Splatting a tuple"
  {% tuple = {4, 5, 6} %}
  p {{*tuple}}

  # p "Double Splatting a hash"
  # {% hash = {"a" => 1, "b" => 2} %}
  # p {{**hash}}
  # ------------
  # Syntax error in expanded macro: splat:11: unexpected token: =>
  #
  # p "a" => 1, "b" => 2
  #       ^

  p "Double Splatting a named tuple"
  {% named_tuple = {"c": 3, "d": 4} %}
  p {{**named_tuple}}
end

splat
# output:
# "Splatting a array"
# 1
# 2
# 3
# "Splatting a tuple"
# 4
# 5
# 6
# "Double Splatting a named tuple"
# {c: 3, d: 4}

展開は、各要素をカンマで区切った形になります。 HashLiteral もそのままカンマ区切りで出力されますが、使い所によっては上記のように Syntax error になります。

定数

マクロは定数にアクセスできます。次のコードを見てください。

CONSTANTS = ["foo", "bar", "baz"]

{% for value in CONSTANTS %}
  puts {{value}}
{% end %}
# output:
# foo
# bar
# baz

一見、マクロ以外の部分をマクロが参照しているので違和感があります。

Crystal は定数の再代入は認めていません。再代入がある場合は、 already initialized constant XXX というエラーでコンパイルに失敗します。つまり、定数の値は不変なのでマクロ解析のフェーズでも扱えるというわけです。

ネストしたマクロ

ネストしたマクロも書くことができます。つまり、「マクロ定義を生成するマクロ」です。

ネストしたマクロは、外側から順に内側に向かって展開されます。その際、内側のマクロは外側のマクロで展開されないように \ でエスケープする必要があります。公式マニュアルの例がわかりやすいので引用します。次のコードを見てください。

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!"

外側のマクロで展開しない部分だけエスケープしていることに注目してください。特に、

"\{{greeting.id}} {{name.id}}"

の部分では、外側のマクロで {{name.id}} の部分は展開されますが、 \{{greeting.id}} の部分は展開されません。\{{greeting.id}} の部分は内側のマクロで展開されます。Nested macros は、マクロの記述で重複が多い場合に有効です。しかし、可読性が損なわれやすいので注意が必要です。

生成コードの注意点

マクロで生成するコードは、それ単体で Crystal のコードとして完結していなければなりません。言い替えれば、 生成されたコードを別のファイルに書き出して正しくパースされるようなコードでなければなりません。この制約は忘れてしまいがちなので気をつけましょう。次の例を見てください。

ret = ""
var = "pitfalls"

ret = case var
  {% for klass in [Int32, String] %}
    when {{ klass.id }} then "#{var} is {{ klass }}"
  {% end %}
end

一見、マクロが展開されたら正しい Crystal のコードが生成されるように見えます。しかし、マクロで生成されるコードは when から始まる部分だけなので、Crystal のコードとしては不完全で、エラーとなります。

この場合は、 {% begin %} …​ {% end %} でコードを括りましょう。

ret = ""

{% begin %}
  var = "pitfalls"

  ret = case var
    {% for klass in [Int32, String] %}
      when {{ klass.id }} then "#{var} is {{ klass }}"
    {% end %}
  end
{% end %}

ret # => "pitfalls is String"

こうすることで、マクロが生成するコードが正しい Crystal のコードとなるため、コンパイルが通るようになります。陥りやすい間違いなので気をつけてください。

型の情報にアクセスできる @type

マクロには特別なインスタンス変数 @type が用意されています。これを使うと、コンパイル時の型情報にアクセスできます。実際どんなメソッドが存在しているかを見たほうがわかりやすいので、いくつかご紹介します。@typeCrystal::Macros::TypeNode クラスです。

TypeNode#instance_vars

型に定義されているインスタンス変数を返します。返り値は Crystal::Macros::MetaVar クラスの配列です。MetaVar クラスは、変数やインスタンス変数を表す型で、名前( MetaVar#name )と型( MetaVar#type )を持っています。

class MyClass
  def initialize(@name : String, @age : Int32)
  end

  def instance_variables_name
    # インスタンス変数の名前を配列で返す
    {{ @type.instance_vars.map(&.name.stringify) }}
  end

  def instance_variables_type
    # インスタンス変数の型を配列で返す
    {{ @type.instance_vars.map(&.type) }}
  end
end

my_class = MyClass.new("Taro", 30)
my_class.instance_variables_name # => ["name", "age"]
my_class.instance_variables_type # => [String, Int32]

TypeNode#methods

型に定義されているメソッドの情報を返します。返り値は Crystal::Macros::Def クラスの配列です。Def クラスは、 def 文を表す型で、メソッド定義に関するさまざまな情報を持っています。例えば、 Def#args は引数の情報、 Def#return_type はメソッドの返り値の型を表します。

class MyClass
  def m1
  end

  protected def m2
  end

  private def m3
  end

  def methods_name
    # 定義されているメソッドの名前を返す
    {{ @type.methods.map(&.name.stringify) }}
  end

  def methods_visibility
    # 定義されているメソッドのアクセス修飾子を返す
    {{ @type.methods.map(&.visibility.stringify) }}
  end
end

my_class = MyClass.new

my_class.methods_name
# => ["m1", "m2", "m3", "methods_name", "methods_visibility"]

my_class.methods_visibility
# => [":public", ":protected", ":private", ":public", ":public"]

これらの他にもメソッドはたくさんあります。私の調べた限りでは、組み合わせればやりたいことはできるという、必要最低限なメソッドはそろっていました。興味のある方はぜひ調べてみてください。

Hooks

一部の特別な名前を持ったマクロは hooks と呼ばれ、特定のタイミングでコンパイル時に実行されます。

テーブル 1. Hooks
マクロ 効果

macro inherited …​ end

サブクラスが定義されたときに実行されるマクロ

macro included …​ end

モジュールが include されたときに実行されるマクロ

macro extended …​ end

モジュールが extend されたときに実行されるマクロ

macro method_added …​ end

メソッドが追加されたときに実行されるマクロ

macro method_missing …​ end

呼び出そうとしたメソッドが定義されていない場合に実行されるマクロ

macro finished …​ end

インスタンス変数の型が決定したあとに呼び出されるマクロ

inherited の例を見てみましょう。

class SuperClass
  macro inherited
    def type_name
      {{ @type.name.stringify }}
    end
  end
end

class SubClass < SuperClass
end

# SuperClass.new.type_name は undefined method 'type_name' for SuperClass となる
SubClass.new.type_name # => "SubClass"

継承した場合のみ実行されるので、 SuperClass には #type_name が存在していないことがわかります。

method_missing の例も見てみましょう。

macro method_missing(call)
  puts "name: {{call.name.id}}"
  puts "args: {{call.args.size}} arguments"
end

foo
# output:
# name: foo
# args: 0 arguments

bar 1, 'a'
# output:
# name: bar
# args: 2 arguments

method_missing の引数は Crystal::Macros::Call です。これはメソッドの呼び出しを表すクラスです。#args#receiver などがあります。

Fresh variables

マクロが展開されると、マクロ内で定義した変数もそのまま展開され、 Crystal コードとして解釈されます。次の例を見てください。

macro update_x
  x = 1
end

x = 0
update_x

x # => 1

これは、ローカル変数を上書きして重複を排除する際には有効です。しかし、ライブラリで提供するマクロなどでは、意図しない形で上書きされてしまう可能性があります。そのため、 fresh variables という構文が用意されています。次の例を見てください。

macro dont_update_x
  %x = 1
  puts %x
end

x = 0
dont_update_x # outputs 1

x # => 0

%変数名 とすることで、そのマクロのコンテキスト内で唯一の変数として扱われます。仕組みは簡単です。上記のコードで crystal tool expand をしてみましょう。

$ crystal tool expand -c /path/to/fresh_variables_example.cr:6:1 /path/to/fresh_variables_example.cr
1 expansion found
expansion 1:
   dont_update_x

# expand macro 'dont_update_x' (/path/to/fresh_variables_example.cr:2:1)
~> __temp_20 = 1
   puts(__temp_20)

__temp_20 のような変数に置き換わっています。このように、マクロの実行フェーズで変数名を置き換えています。

まとめ

以上で、マクロとはどういうものか、マクロの文法はどうなっているのかなどの説明を終わります。マクロのおおまかなイメージは湧きましたでしょうか。自分はライブラリを書く際にマクロを使いますが、やはり重複排除の効果はすごいと思います。また、 DSL の提供も比較的簡単にできるのではないでしょうか。マクロに慣れてくると、 Crystal 本来のコードを書くよりもマクロを書いている比率が多くなる印象が強いです。今回のこの章を読み、「マクロが読めるようになった」「マクロが書けるようになった」という方が一人でも増えて頂けると幸いです。