LoginSignup
11
11

More than 5 years have passed since last update.

ElixirのDSLの基本構造。(Ecto.Schemaのネストしたマクロはどう作られている?)

Last updated at Posted at 2016-03-11

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"

問題なく呼び出すことが出来ます。func1func2も正常に定義されています。マクロからマクロを呼び出すことは可能だということが分かりました。

サンプル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が使われていたりします。ここら辺に関しては是非コードを実際に読んでみて頂ければと思います。

11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11