[Elixir]Phoenix.Router付近やendpointを追ってみる

Phoenixのrouterにおける、macroによる動的な関数定義を追ってみました。その備忘録。

macroを多分に使っているといわれるPhoenixの入り口を覗いて仕組みを少し知ることが目的です。きっと、ここら辺追えるとテストフレームワークとかそこらへんも追ったり作れるだろうと踏んで。

まず、入り口のおさらいから。

macroによる定義のおさらい

  • Sample
  • Caller

の2種類のモジュールを定義します。そのうち、 Sample では様々な方法で関数を定義します。Caller では、その Sampleuse して、定義されたマクロの1つである my_def を使い関数を定義、実施します。こうなると、例えば my_defget などで置き換わった時、その引数となった文字列を関数のように扱いそのブロックを実行する、ということも可能になります。
(最後には実際の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に潜ってみます。

  @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で関数として定義されそうですね。

ちょっと長いですが、以下で 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の入り口には立てたかな。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中