はじめに
Elixir の Plug は、モジュールを組み合わせるために以下の DSL を提供している:
defmodule MyModule do
use Plug.Builder
plug :hello
plug :world, good: :morning
end
use Plug.Builder したモジュール内で plug :atom, [optional args] と書くことで、モジュールに他の plug を組み込むことができる。
マクロを駆使して実装された plug の中身がどうなっているのか、 Plug.Builder を再実装しながら追ってみることにした。
マクロの参考資料
MyPlugBuilder の概要
Plug.Builder を丸ごと再実装するのは骨が折れるので、 plug マクロだけを提供するように単純化したモジュール MyPlugBuilder を作ることにする。
MyPlugBuilder は以下のように使う:
defmodule MyModule do
use MyPlugBuilder
plug :hello
plug :world, good: :morning
end
IO.puts "plugs >>> #{inspect MyApp.plugs}"
# => plugs >>> [world: [good: :morning], hello: []]
defmodule 内で use MyPlugBuilder すると、 Plug.Builder に似た plug マクロを使えるようになる。同じ defmodule 内でなら何回でも plug を書ける。
定義したモジュールには plugs 関数が生える。この関数は、 plug に与えた atom とオプション引数のタプルをリストにして返す。
Plug.Builder はこのリストを元にモジュールを組み合わせていくのだが、今回はその処理までは再現しないことにする。
MyPlugBuilder を実装する
実装の過程は省くが、 MyPlugBuilder の実装は以下のようになる。
defmodule MyPlugBuilder do
# `use MyPluginBuilder` すると呼ばれる
defmacro __using__(_opts) do
quote do
import MyPlugBuilder, only: [plug: 1, plug: 2]
Module.register_attribute(__MODULE__, :plugs, accumulate: :true)
@before_compile MyPlugBuilder
end
end
# `plug` 本体
defmacro plug(plug, opts \\ []) do
quote do
@plugs {unquote(plug), unquote(opts)}
end
end
# `@before_compile MyPlugBuilder` 属性を含む翻訳単位がコンパイルされる直前に呼ばれる
defmacro __before_compile__(env) do
plugs = Module.get_attribute(env.module, :plugs)
quote do
def plugs, do: unquote(plugs)
end
end
end
この MyPlugBuilder を use して plug を使うとマクロがどのように展開されていくか、順を追って見ていくことにしよう。
マクロの展開を追う
1. 初期状態
上で書いた MyPlugBuilder の使い方を再掲する。ここから1段階ずつマクロを展開していく。
defmodule MyModule do
use MyPlugBuilder
plug :hello
plug :world, good: :morning
end
2. use MyPlugBuilder マクロを展開する
use は Elixir の組み込みマクロである。 use MyPlugBuilder は require MyPlugBuilder に展開される。また、指定したモジュールが __using__/1 を定義しているなら、 use に与えたオプション引数リストを引数にしてそれを呼ぶ。
defmodule MyModule do
# ----
# use MyPlugBuilder
# ---- ↓
require MyPlugBuilder
MyPlugBuilder.__using__([])
# ----
plug :hello
plug :world, good: :morning
end
3. MyPlugBuilder.__using__ マクロを展開する
MyPlugBuilder.__using__ マクロは以下のように定義したのだった:
defmacro __using__(_opts) do
quote do
import MyPlugBuilder, only: [plug: 1, plug: 2]
Module.register_attribute(__MODULE__, :plugs, accumulate: :true)
@before_compile MyPlugBuilder
end
end
マクロは関数と似ているが、マクロの返り値は Elixir の式の内部表現でなければならない。式の内部表現は quote do 式 end で得ることができる(「式を quote する」という)。マクロが返した内部表現はマクロを呼び出した側に埋め込まれる。
MyPlugBuilder.__using__ マクロは単に quote された式を返す。それを呼び出し側に埋め込むとこうなる:
defmodule MyModule do
require MyPlugBuilder
# ----
# MyPlugBuilder.__using__([])
# ---- ↓
import MyPlugBuilder, only: [plug: 1, plug: 2]
Module.register_attribute(__MODULE__, :plugs, accumulate: :true)
@before_compile MyPlugBuilder
# ----
plug :hello
plug :world, good: :morning
end
import によって defmodule MyModule ブロック内で plug マクロを使えるようになる。
また Module.register_attribute によって、 defmodule ブロック内の @plugs 属性に指定された値を後で参照できるようになる。
@before_compile 属性のはたらきは後述する。
4. plug マクロを展開する
plug マクロの定義はこうだ:
defmacro plug(plug, opts \\ []) do
quote do
@plugs {unquote(plug), unquote(opts)}
end
end
全体を囲う quote の中で、一部を unquote している。 unquote(plug) はマクロの引数をこの場所にそのまま埋め込むという意味だ。 plug マクロを展開すると結局 @plugs 属性になる:
defmodule MyModule do
require MyPlugBuilder
import MyPlugBuilder, only: [plug: 1, plug: 2]
Module.register_attribute(__MODULE__, :plugs, accumulate: :true)
@before_compile MyPlugBuilder
# ----
# plug :hello
# plug :world, good: :morning
# ---- ↓
@plugs {:hello, []}
@plugs {:world, [good: :morning]}
# ----
end
5. MyPlugBuilder.__before_compile__ マクロを注入する
import と require はスペシャルフォームマクロ(処理系に特別扱いされるマクロ)だ。普通のマクロのようには展開されないので、ここでは中身に深入りせず、展開が終わったということにしておく。
さて、すべてのマクロの展開が終わったので MyModule モジュールのコンパイルが始まる。だがその前にコンパイラは @before_compile MyPlugBuilder の処理を行う。
@before_compile 属性にモジュールを指定したとき、コンパイラはまずそのモジュールの __before_comiple__/1 マクロを呼び出す。引数は __ENV__ となる。そしてその返り値を @before_compile を含むモジュールの末尾に注入する。(参考: http://elixir-lang.org/docs/stable/elixir/Module.html )
言い替えれば、モジュール内のどこかに @before_compile MyPlugBuilder を書けば、モジュール末尾に MyPlugBuilder.__before_compile__(__ENV__) を書いたのと同じということだ:
defmodule MyModule do
require MyPlugBuilder
import MyPlugBuilder, only: [plug: 1, plug: 2]
Module.register_attribute(__MODULE__, :plugs, accumulate: :true)
# この属性によって……
@before_compile MyPlugBuilder
@plugs {:hello, []}
@plugs {:world, [good: :morning]}
# この位置に以下を書いたのと同じことになる
MyPlugBuilder.__before_compile__(__ENV__)
end
6. MyPlugBuilder.__before_compile__ マクロを展開する
MyPlugBuilder.__before_compile__ の定義はこうだ:
defmacro __before_compile__(env) do
plugs = Module.get_attribute(env.module, :plugs)
quote do
def plugs, do: unquote(plugs)
end
end
Module.get_attribute によって、 env.module の(つまり MyModule の) @plugs 属性に指定された値を得ておく。このマクロはその値を返す plugs 関数に展開される:
defmodule MyModule do
require MyPlugBuilder
import MyPlugBuilder, only: [plug: 1, plug: 2]
Module.register_attribute(__MODULE__, :plugs, accumulate: :true)
@before_compile MyPlugBuilder
@plugs {:hello, []}
@plugs {:world, [good: :morning]}
# ----
# MyPlugBuilder.__before_compile__(__ENV__)
# ---- ↓
def plugs, do: [world: [good: :morning], hello: []]
# ----
end
7. コンパイル終了
モジュールをコンパイルすると属性は取り除かれる。最終的に MyModule は以下の形になる:
defmodule MyModule do
require MyPlugBuilder
import MyPlugBuilder, only: [plug: 1, plug: 2]
Module.register_attribute(__MODULE__, :plugs, accumulate: :true)
def plugs, do: [world: [good: :morning], hello: []]
end
おわりに
マクロを駆使することで以下のようにコードを変換できることが分かった:
defmodule MyModule do
use MyPlugBuilder
plug :hello
plug :world, good: :morning
end
# ↓↓↓
defmodule MyModule do
require MyPlugBuilder
import MyPlugBuilder, only: [plug: 1, plug: 2]
Module.register_attribute(__MODULE__, :plugs, accumulate: :true)
def plugs, do: [world: [good: :morning], hello: []]
end
MyPlugBuilder は簡単のために plug マクロを def plugs に変換するだけとしたが、 Plug.Builder はモジュールを組み合わせるために追加の処理を行っている。とは言え、肝心の plug マクロの展開の流れは MyPlugBuilder で十分に再現できたと思う。
感想
マクロは面白いけど使いたくない。