Merry Christmas!
gumi Inc. 2018 Advent Calendar の栄えある最終日を担当いたします幾田です. よろしくお願いします. 初日は Elixir で幕開けさせていただきましたが, 最終日も Elixir で幕引きさせていただきます.
この記事について
この記事は, Elixir のマクロを読もう1 から始まった 1 Elixir のマクロを読もうシリーズの二回目となります.
前回の記事 では, マクロ定義の基礎的な概念 (AST) や道具 (quote/2, unquote/1, defmacro/2) について解説したため, この記事では, もう少し踏み込んだ解説を行っていきます.
この記事を読み終えると, gen_server.ex に定義されている GenServer モジュールのマクロに関連するコードが読めるようになります.
Elixir マクロ初心者の方は, この記事から読み始めることはできません. かならず, 前回の記事 から順番に読み進めてください.
require/2
マクロが定義される目的として, 最多のものは「def 定義をモジュールに取り込む」ことである.
単に def 定義を取り込むだけであれば, require/2 や import/2 で def 定義を差し込むマクロを使用可能にし, そのマクロを使用するとよい.
require/2, import/2 の詳細ついては, こちら を参照すること.
次のファイルを保存し, iex 上で動作確認してほしい.
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
iex> c "exsample01.ex"
[Greeting, ExtendChristmasDef]
iex> Greeting.say_greeting_for_christmas
Merry Christmas!
:ok
use/2
筆者は, require/2 を使用する方が明示的で好ましいと考えるが, use/2 を用いることで前述を少しだけ短縮できる.
次のファイルを保存し, iex 上で動作確認してほしい.
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
iex> c "exsample02.ex"
[Greeting, ExtendChristmasDef]
iex> Greeting.say_greeting_for_christmas
Merry Christmas!
:ok
use/2 とは何だろうか? use/2 の正体を探るため, iex を起動し, 次のコードを実行して結果を確認してほしい.
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 は, 前回の記事 を参照すること.
@before_compile
__using__/1 内でよく行われることに, モジュールアトリビュートの操作がある.
次のファイルを保存し, iex 上で動作確認してほしい.
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
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 上で動作確認してほしい.
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
iex> c "exsample04.ex"
[Greeting, ExtendGreetDef]
iex> Greeting.say
Happy New Year!
Merry Christmas!
:ok
正確ではないが, マクロが展開されていく大凡のイメージを次に示す.
# マクロ展開前
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 master ブランチのものである.
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 を起動し, 次のコードを実行して結果を確認してほしい.
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 を起動し, 次のコードを実行して結果を確認してほしい.
iex> quote do: A.{B, C}
{{:., [], [{:__aliases__, [alias: false], [:A]}, :{}]}, [],
[{:__aliases__, [alias: false], [:B]}, {:__aliases__, [alias: false], [:C]}]}
:{} がコード表現に入るパターンは, タプルであるため, タプルを利用した複数モジュールを一括処理する構文のためだと勘が働く.2
とはいえ, commit を読めば一目瞭然ではある.
結果, expand_aliases/2 に A.{B, C} を渡すと [A.B, A.C] というリストが戻り, Enum.map/2 から require/2 と __using__/1 するコード表現をリストで受け取ることになる.
最後に, unquote_splicing/1 でコード表現のリストを, コード表現に戻す.
# 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__([])
)
)