Ectoでモデルを定義する際は、下記のようなDSLを使用します。
defmodule User do
use Ecto.Schema
schema "users" do
field :name, :string
field :age, :integer
end
end
この類の、schema
というマクロの下にさらにfield
というマクロが入れ子になっている構造は、いったいどのような仕組みで実現されているのか、下記いくつかのサンプルでご説明させて頂きたいと思います。
サンプル1
defmodule A do
defmacro __using__(_opts) do
quote do
import A
end
end
defmacro macro1 do
quote do
def func1 do
IO.inspect "func1 called"
end
end
end
defmacro macro2 do
quote do
def func2 do
IO.inspect "func2 called"
end
end
end
end
defmodule B do
use A
macro1
macro2
end
B.func1
# => "func1 called"
B.func2
# => "func2 called"
ごくごくシンプルなマクロです。Aモジュールをuseしてmacro1
を呼び出すとfunc1
が定義され、macro2
を呼び出すとfunc2
が定義されます。
サンプル2
次に、上記サンプル1のmacro1のquote式内で、macro2を呼び出してみましょう。
defmodule A do
defmacro __using__(_opts) do
quote do
import A
end
end
defmacro macro1 do
quote do
def func1 do
IO.inspect "func1 called"
end
A.macro2 # macro2を呼び出す
end
end
defmacro macro2 do
quote do
def func2 do
IO.inspect "func2 called"
end
end
end
end
defmodule B do
use A
macro1
end
B.func1
"func1 called"
B.func2
"func2 called"
問題なく呼び出すことが出来ます。func1
もfunc2
も正常に定義されています。マクロからマクロを呼び出すことは可能だということが分かりました。
サンプル3
それでは本題です。マクロ内からマクロを呼び出せることが分かりましたので、次はmacro1
のdoブロック引数にmacro2
の呼び出しを埋め込んでみましょう。
defmodule A do
defmacro __using__(_opts) do
quote do
import A
end
end
defmacro macro1 [do: block]do
quote do
def func1 do
IO.inspect "func1 called"
end
unquote(block) # 引数で渡されたdoブロックを展開する
end
end
defmacro macro2 do
quote do
def func2 do
IO.inspect "func2 called"
end
end
end
end
defmodule B do
use A
macro1 do
macro2 # doブロックにmacro2の呼び出しを含める
end
end
B.func1
"func1 called"
B.func2
"func2 called"
無事に実行されました。
サンプル4
最後のサンプルです。サンプル3までに得た情報を使って、Ecto.Schemaと似た感じのDSLを定義できるモジュールを作成してみましょう。
(下記で使用しているModule attributes
という機能に関しては、こちらの情報等をご参照ください)
defmodule Ecto.Schema do
defmacro __using__(_opts) do
quote do
import Ecto.Schema
Module.register_attribute __MODULE__, :fields, accumulate: true
@before_compile unquote(__MODULE__)
end
end
defmacro schema(_source, [do: block]) do
quote do
unquote(block)
end
end
defmacro field(name) do
quote do
@fields unquote(name)
end
end
defmacro __before_compile__(env) do
fields = Module.get_attribute(env.module, :fields) |> Enum.reverse
quote do
def get_fields, do: unquote(Macro.escape(fields))
end
end
end
defmodule User do
use Ecto.Schema
schema "user" do
field "col1"
field "col2"
end
end
User.get_fields
# => ["col1", "col2"]
schema
マクロとfield
マクロを入れ子にして、field
マクロに指定した列名のリストを、get_fields
という関数を使用して取得することが出来ました。
まとめ
ElixirのDSLにおけるマクロのネスト構造の基本は、上記のようにdoブロックで別のマクロを渡してそれをquote内でunquoteするという方式になるということがお分かり頂けたかと思います。
備考
上記の擬似的なEcto.Schemaモジュールに関しては、説明のためかなり簡略化してありますので、実際のEcto.Schemaモジュールがこの構造になっていることを保証するものではありません。
例えば実際のEcto.Schemaモジュールでは__before_compile__
マクロは使用されていませんし、unquote
以外にもeval_quoted
が使われていたりします。ここら辺に関しては是非コードを実際に読んでみて頂ければと思います。