Elixir のマクロを読もう2

Merry Christmas!

gumi Inc. 2018 Advent Calendar の栄えある最終日を担当いたします幾田です. よろしくお願いします. 初日は Elixir で幕開けさせていただきましたが, 最終日も Elixir で幕引きさせていただきます.

# この記事について

この記事は, [Elixir のマクロを読もう1](https://qiita.com/cooldaemon/items/23537c679804e050d698) から始まった [^1] Elixir のマクロを読もうシリーズの二回目となります.

[前回の記事](https://qiita.com/cooldaemon/items/23537c679804e050d698) では, マクロ定義の基礎的な概念 (AST) や道具 (quote/2, unquote/1, defmacro/2) について解説したため, この記事では, もう少し踏み込んだ解説を行っていきます.

この記事を読み終えると, gen_server.ex に定義されている GenServer モジュールのマクロに関連するコードが読めるようになります.

Elixir マクロ初心者の方は, この記事から読み始めることはできません. かならず, [前回の記事](https://qiita.com/cooldaemon/items/23537c679804e050d698) から順番に読み進めてください.

# require/2

マクロが定義される目的として, 最多のものは「def 定義をモジュールに取り込む」ことである.

単に def 定義を取り込むだけであれば, require/2 や import/2 で def 定義を差し込むマクロを使用可能にし, そのマクロを使用するとよい.

require/2, import/2 の詳細ついては, [こちら](https://dev.to/gumi/elixir-13-aliasrequireimport-55c1) を参照すること.

次のファイルを保存し, iex 上で動作確認してほしい.

```elixir:exsample01.ex
defmodule ExtendChristmasDef do
  defmacro extend_christmas_def do
    quote do
      def say_greeting_for_christmas do
        IO.puts "Merry Christmas!"
      end
    end
  end
end

defmodule Greeting do
  require ExtendChristmasDef # 定義済のマクロを使用可能にする
  ExtendChristmasDef.extend_christmas_def # マクロで say_greeting/0 を取り込む
end
```

```elixir
iex> c "exsample01.ex"
[Greeting, ExtendChristmasDef]

iex> Greeting.say_greeting_for_christmas
Merry Christmas!
:ok
```

# use/2

筆者は, require/2 を使用する方が明示的で好ましいと考えるが, use/2 を用いることで前述を少しだけ短縮できる.

次のファイルを保存し, iex 上で動作確認してほしい.

```elixir:exsample02.ex
defmodule ExtendChristmasDef do
  defmacro __using__(_options) do
    quote do
      def say_greeting_for_christmas do
        IO.puts "Merry Christmas!"
      end
    end
  end
end

defmodule Greeting do
  use ExtendChristmasDef # なんと一行に!
end
```

```elixir
iex> c "exsample02.ex"
[Greeting, ExtendChristmasDef]

iex> Greeting.say_greeting_for_christmas
Merry Christmas!
:ok
```

use/2 とは何だろうか？ use/2 の正体を探るため, iex を起動し, 次のコードを実行して結果を確認してほしい.

```elixir
iex> code_expression = quote do: use ExtendChristmasDef

iex> code_expression |> Macro.expand_once(__ENV__) |> Macro.to_string
(
  require(ExtendChristmasDef)
  ExtendChristmasDef.__using__([])
)

iex> code_expression = quote do: use ExtendChristmasDef, foo: :bar, baz: :quu

iex> code_expression |> Macro.expand_once(__ENV__) |> Macro.to_string
(
  require(ExtendChristmasDef)
  ExtendChristmasDef.__using__(foo: :bar, baz: :quu)
)
```

見てのとおり use/2 は, require/2 と \_\_using\_\_/1 に展開される.

use/2 は一般的によく使用されるのだが, Ecto や Plug などでは, \_\_using\_\_/1 内で use/2, import/2, alias/2 などが多用されており, 正確に挙動を把握するには, かなり広範囲に渡りコードを読む必要がある. マクロの辛さは, use/2 に凝縮されている.

なお, Macro.expand_once/2 は, コード表現に一回だけマクロを適用して展開するため, このようにマクロの動作確認などに使用できる. 最後まで展開する Macro.expand/2 も存在する.

Macro.to_string/1 は, [前回の記事](https://qiita.com/cooldaemon/items/23537c679804e050d698) を参照すること.

# @before_compile

\_\_using\_\_/1 内でよく行われることに, モジュールアトリビュートの操作がある.

次のファイルを保存し, iex 上で動作確認してほしい.

```elixir:exsample03.ex
defmodule ExtendGreetDef do
  defmacro __using__(_options) do
    quote do
      # greet/2 マクロを簡単に呼べるよう import/2 を使用
      import unquote(__MODULE__)

      # モジュールのアトリビュートを登録
      # リストとして定義
      # greet/2 マクロで定義された def を保持する
      Module.register_attribute __MODULE__, :greet_defs, accumulate: true

      # greet/2 マクロで定義された def を全て実行する
      def say do
        Enum.each @greet_defs, fn greet_def ->
          apply(__MODULE__, greet_def, [])
        end
      end
    end
  end

  defmacro greet(name, message) do
    quote do
      @greet_defs unquote(name) # アトリビュートに def の名前を追加
      def unquote(name)() do
        IO.puts unquote(message)
      end
    end
  end
end

defmodule Greeting do
  use ExtendGreetDef

  greet :for_christmas, "Merry Christmas!"
  greet :for_new_year, "Happy New Year!"
end
```

```elixir
iex> c "exsample03.ex"
[Greeting, ExtendGreetDef]

iex> Greeting.for_christmas
Merry Christmas!
:ok

iex> Greeting.for_new_year
Happy New Year!
:ok

iex> Greeting.say
:ok
```

greet/2 による def 定義までは意図した通りに動作しているが, 最後に @greet_defs に登録されている def を実行するところが意図した通りに動作していない. これは, say/0 の定義時に @greet_defs が空リストであることが原因である.

解決策として, greet/2 による @greet_defs へのリスト追加を全て終えてから say/0 を定義できるとよい. これを実現するため, 全てのマクロ展開が終わった後, かつ, コンパイルの寸前に呼ばれるフック @before_compile を使用する.

次のファイルを保存し, iex 上で動作確認してほしい.

```elixir:exsample04.ex
defmodule ExtendGreetDef do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)

      Module.register_attribute __MODULE__, :greet_defs, accumulate: true

      # このアトリビュートにモジュール名を指定すると,
      # コンパイル前に, __before_compile__/1 が実行される.
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(_env) do
    # __before_compile__ 返されたコード表現は,
    # 呼び出したモジュールの末尾に追加される.
    quote do
      def say do
        Enum.each @greet_defs, fn greet_def ->
          apply(__MODULE__, greet_def, [])
        end
      end
    end
  end

  defmacro greet(name, message) do
    quote do
      @greet_defs unquote(name)
      def unquote(name)() do
        IO.puts unquote(message)
      end
    end
  end
end

defmodule Greeting do
  use ExtendGreetDef

  greet :for_christmas, "Merry Christmas!"
  greet :for_new_year, "Happy New Year!"
end
```

```elixir
iex> c "exsample04.ex"
[Greeting, ExtendGreetDef]

iex> Greeting.say
Happy New Year!
Merry Christmas!
:ok
```

正確ではないが, マクロが展開されていく大凡のイメージを次に示す.

```elixir
# マクロ展開前

defmodule Greeting do
  use ExtendGreetDef

  greet :for_christmas, "Merry Christmas!"
  greet :for_new_year, "Happy New Year!"
end

# use が展開される

defmodule Greeting do
  require(ExtendGreetDef)
  ExtendGreetDef.__using__([])

  greet :for_christmas, "Merry Christmas!"
  greet :for_new_year, "Happy New Year!"
end

# __using__/1 が展開される
# require/2 は特殊であるため割愛

defmodule Greeting do
  import ExtendGreetDef

  Module.register_attribute ExtendGreetDef, :greet_defs, accumulate: true

  @before_compile ExtendGreetDef

  greet :for_christmas, "Merry Christmas!"
  greet :for_new_year, "Happy New Year!"
end

# greet が展開される
# import/2 は特殊であるため割愛

defmodule Greeting do
  Module.register_attribute ExtendGreetDef, :greet_defs, accumulate: true

  @before_compile ExtendGreetDef

  @greet_defs :for_christmas
  def for_christmas() do
    IO.puts "Merry Christmas!"
  end

  @greet_defs :for_new_year
  def for_new_year() do
    IO.puts "Happy New Year!"
  end
end

# @before_compile が処理される

defmodule Greeting do
  Module.register_attribute ExtendGreetDef, :greet_defs, accumulate: true

  @greet_defs :for_christmas
  def for_christmas() do
    IO.puts "Merry Christmas!"
  end

  @greet_defs :for_new_year
  def for_new_year() do
    IO.puts "Happy New Year!"
  end

  def say do
    Enum.each @greet_defs, fn greet_def ->
      apply(__MODULE__, greet_def, [])
    end
  end
end

# @greet_defs が展開される

defmodule Greeting do
  def for_christmas() do
    IO.puts "Merry Christmas!"
  end

  def for_new_year() do
    IO.puts "Happy New Year!"
  end

  def say do
    Enum.each [:for_new_year, :for_christmas], fn greet_def ->
      apply(__MODULE__, greet_def, [])
    end
  end
end
```

# おわりに

ここまでで, GenServer モジュールのマクロに関連するコードが読めるようになるため, 今回のマクロ解説は終わりとします.

Advent Calendar は終わってしまいますが, Elixir のマクロを読もうシリーズは続けていく予定です. もう少々お付き合いいただければ幸いです. 次回は, GenServer モジュールの解説か, defmacro/2 を使わないマクロ定義の解説を行う予定です.

# 追記

今回も記事を書いている最中に, 基礎情報の範囲外として除外した内容を追記します.

## use/2 のコードを読む

Macro.expand/2 で展開結果だけを眺めるのではなく, コードを読んで動作を把握しておくとよい. use/2 は, kernel.ex で定義されている.

なお, 次のコードは, この記事を執筆時点(2018年12月23日)の [Elixir](https://github.com/elixir-lang/elixir) master ブランチのものである.

```elixir:kernel.ex
defmacro use(module, opts \\ []) do
  calls =
    Enum.map(expand_aliases(module, __CALLER__), fn
      expanded when is_atom(expanded) ->
        quote do
          require unquote(expanded)
          unquote(expanded).__using__(unquote(opts))
        end

      _otherwise ->
        raise ArgumentError,
              "invalid arguments for use, " <>
                "expected a compile time atom or alias, got: #{Macro.to_string(module)}"
    end)

  quote(do: (unquote_splicing(calls)))
end

defp expand_aliases({{:., _, [base, :{}]}, _, refs}, env) do
  base = Macro.expand(base, env)

  Enum.map(refs, fn
    {:__aliases__, _, ref} ->
      Module.concat([base | ref])

    ref when is_atom(ref) ->
      Module.concat(base, ref)

    other ->
      other
  end)
end

defp expand_aliases(module, env) do
  [Macro.expand(module, env)]
end
```

expand_aliases/2 で, コード表現で渡ってくるモジュール名をアトムに戻している. ここで注目するべきはリスト処理となっていること.

iex を起動し, 次のコードを実行して結果を確認してほしい.

```elixir
iex> code_expression = quote do: A.B.C
{:__aliases__, [alias: false], [:A, :B, :C]}

iex> Macro.expand(code_expression, __ENV__)
A.B.C
```

A.B.C のような一般的なモジュール名であれば, Macro.expand/2 でアトムに戻る.

では, なぜリスト処理なのか？ これは, 「{{:., _, \[base, :{}\]}, _, refs}」というコード表現のパース処理にヒントがある.

iex を起動し, 次のコードを実行して結果を確認してほしい.

```elixir
iex> quote do: A.{B, C}
{{:., [], [{:__aliases__, [alias: false], [:A]}, :{}]}, [],
 [{:__aliases__, [alias: false], [:B]}, {:__aliases__, [alias: false], [:C]}]}
```

:{} がコード表現に入るパターンは, タプルであるため, タプルを利用した複数モジュールを一括処理する構文のためだと勘が働く.[^2]

とはいえ, [commit](https://github.com/elixir-lang/elixir/commit/8c57c9110301c1ee02d84b574c59feff00e14ba3) を読めば一目瞭然ではある.

結果, expand_aliases/2 に A.{B, C} を渡すと \[A.B, A.C\] というリストが戻り, Enum.map/2 から require/2 と \_\_using\_\_/1 するコード表現をリストで受け取ることになる.

最後に, unquote_splicing/1 でコード表現のリストを, コード表現に戻す.

```elixir
# calls の値イメージ (実際には, コード表現が入っている)

[(
  require(A.B)
  A.B.__using__([])
), (
  require(A.C)
  A.C.__using__([])
)]


# unquote_splicing/1 を行った後の値イメージ

(
  (
    require(A.B)
    A.B.__using__([])
  )
  (
    require(A.C)
    A.C.__using__([])
  )
)
```

[^1]: 続くと良いな. 続くようにがんばります.
[^2]: 勘が働かない？ blame して commit を読みましょう.