Phoenixのrouterにおける、macroによる動的な関数定義を追ってみました。その備忘録。
macroを多分に使っているといわれるPhoenixの入り口を覗いて仕組みを少し知ることが目的です。きっと、ここら辺追えるとテストフレームワークとかそこらへんも追ったり作れるだろうと踏んで。
まず、入り口のおさらいから。
macroによる定義のおさらい
SampleCaller
の2種類のモジュールを定義します。そのうち、 Sample では様々な方法で関数を定義します。Caller では、その Sample を use して、定義されたマクロの1つである my_def を使い関数を定義、実施します。こうなると、例えば my_def が get などで置き換わった時、その引数となった文字列を関数のように扱いそのブロックを実行する、ということも可能になります。
(最後には実際のPhoneixを使い、追ってみます。)
SampleとCallerのモジュール定義
サンプルコードを以下に貼り。注釈でそれぞれのメモを追加してます。
defmodule Sample do
@value "pig" # こういう定義した値でも関数名に使える、という例のためのもの
defmacro __using__(_) do
quote do
# このなかで宣言されたことを、useされたモジュールの中で行います。
import unquote(__MODULE__)
# http://elixir-lang.org/docs/stable/elixir/Module.html#register_attribute/3
Module.register_attribute __MODULE__, :my_sample, accumulate: true
# useさたモジュールの関数としてコンパイルされます
def run do
IO.puts "running"
end
end
end
# すべて、ASTを実行した形で出力する関数
defmacro inu() do
quote do
unquote(neko())
end
end
# quoteの外はそのまま出力される
# quoteの中はASTで出力される
def neko() do
IO.puts "called before qupte in neko()"
quote do
IO.puts "called between quote in neko()"
end
end
# my_defというmacroを宣言する
# その中身は、 @tests にlistとして保存され、
# compile timeでmoduleに登録され、useされたモジュールで呼び出される
defmacro my_def(def_name, do: test_block) do
my_func = String.to_atom(def_name)
quote do
@my_sample {unquote(my_func), unquote(def_name)}
def unquote(my_func)(), do: unquote(test_block)
end
end
# 動的に関数の名前を定義して、それを生成する
["a", "b", "c"]
|> Enum.each(fn num ->
def unquote(String.to_atom(@value <> "_" <> num))() do
IO.puts("called in pig" <> "_" <> unquote(num))
end
end)
end
defmodule Caller do
use Sample
# Sample内で定義したmacroである"my_def"を使い、 "my_neko" というpublicな関数を定義します。
my_def "my_neko" do
IO.puts "call my_def in Caller"
end
end
これを sample.exs と保存して、以下のCLIを実行します。
iex> c "sample.exs" [Caller, Sample] iex> import Sample iex> import Caller
Sampleの実行それぞれ
補完される内容
iex> Sample. __using__/1 inu/0 my_def/2 neko/0 pig_a/0 pig_b/0 pig_c/0
neko() は、quoteで囲まれたところはASTとして得られます。
iex> Sample.neko
called before qupte in neko()
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [],
["called between quote in neko()"]}
inu() は、quoteのなかで、unquoteしてneko()を呼んでいます。
iex> Sample.inu called before qupte in neko() called between quote in neko() # neko()ではここはASTとして出力さますが、inuではunquoteされているのでそのまま出力されます。 :ok
pig_a などは、 def unquote(String.to_atom(@value "_" num))() do と :atom として定義、unquoteされた要素は動的に関数として定義されます。
iex> Sample.pig_a called in pig_a :ok
Callerの実行
補完される内容
iex> Caller. my_neko/0 run/0
my_def で定義した my_neko は、 @my_sample に要素として登録され、compile timeで MODULE の関数として登録されたものです。
iex> Caller.my_neko call my_def in Caller :ok
__using__ で定義されたrunを実行
iex> Caller.run running :ok
ここまで読むと、だいたい関数として呼ぶことができる定義がどのようなバリエーションで作られるか把握できます。
Phoenixをみてみる
ちょっと、Phoenixに潜ってみます。
- lib/phoenix/router.ex
@doc false
defmacro __using__(_) do
quote do
unquote(prelude())
unquote(defs())
unquote(match_dispatch())
end
end
__using__ で読み込まれている要素を少しみてみます。
- lib/phoenix/router.ex
defp prelude() do
quote do
Module.register_attribute __MODULE__, :phoenix_routes, accumulate: true
@phoenix_forwards %{}
import Phoenix.Router
import Plug.Conn
import Phoenix.Controller
# Set up initial scope
@phoenix_pipeline nil
Phoenix.Router.Scope.init(__MODULE__)
@before_compile unquote(__MODULE__)
end
end
Module.register_attribute を呼んでいるので、 @phoenix_routes に登録される要素はcompile timeで関数として定義されそうですね。
- lib/phoenix/router.ex#L195-L240
ちょっと長いですが、以下で get とか、 post とかとそのendpointを追加していってます。
(ちょっと、そこまで根掘り葉掘り追っているわけではないですが…)
defp defs() do
quote unquote: false do
var!(add_route, Phoenix.Router) = fn route ->
exprs = Route.exprs(route)
@phoenix_routes {route, exprs}
defp match(var!(conn), unquote(exprs.verb_match), unquote(exprs.path),
unquote(exprs.host)) do
unquote(exprs.dispatch)
end
end
var!(add_resources, Phoenix.Router) = fn resource ->
path = resource.path
ctrl = resource.controller
opts = resource.route
if !resource.singleton do
param = resource.param
Enum.each resource.actions, fn
:index -> get path, ctrl, :index, opts
:show -> get path <> "/:" <> param, ctrl, :show, opts
:new -> get path <> "/new", ctrl, :new, opts
:edit -> get path <> "/:" <> param <> "/edit", ctrl, :edit, opts
:create -> post path, ctrl, :create, opts
:delete -> delete path <> "/:" <> param, ctrl, :delete, opts
:update ->
patch path <> "/:" <> param, ctrl, :update, opts
put path <> "/:" <> param, ctrl, :update, Keyword.put(opts, :as, nil)
end
else
Enum.each resource.actions, fn
:show -> get path, ctrl, :show, opts
:new -> get path <> "/new", ctrl, :new, opts
:edit -> get path <> "/edit", ctrl, :edit, opts
:create -> post path, ctrl, :create, opts
:delete -> delete path, ctrl, :delete, opts
:update ->
patch path, ctrl, :update, opts
put path, ctrl, :update, Keyword.put(opts, :as, nil)
end
end
end
end
end
他にも定義が散見されますが、ひとまずここまで。
これら、atomとして定義されるということはあれ、定義できる要素に上限ある?とふと思った次第。
締め
ひとまず、動的な定義に関して追ってみました。一応、簡単なものは読み書きできるようになったかなーという印象。Elixirの入り口には立てたかな。