はじめに
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 で十分に再現できたと思う。
感想
マクロは面白いけど使いたくない。