スポンサーリンク

2015年8月30日

[Elixir]Macro to expand the function

とある錬金術師の万能薬(Elixir)

Goal

Elixirのマクロで、関数を展開するサンプルを作成する。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.5

Wait a minute

このサンプルは、マクロで関数を生成し展開していくサンプルです。
このサンプル自体で何かができるものではありません。
予め、ご了承下さい。

Index

Macro to expand the function
|> Plan
|> First step
|> Second step
|> Brief description

Plan

やりたいことを最初に記述します。
以下のような記述をすると、
alias_name:の値が関数名になり、マクロが展開されていくようにしたい。
defmodule UseSample do
  use Sample

  get "path/to/hoge", "ActionModule", [alias_name: "hoge"]
  get "path/to/huge", "ActionModule", [alias_name: "huge"]
end
上記のalias_name:だと、
hoge_def/1、huge_def/1といったように・・・関数として展開していって欲しい。

First step

第一段階のソースコードを作成します。
Example:
defmodule Sample do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)
    end
  end

  defmacro match(_http_method, _path, _action, opts) do
    name = opts[:alias_name]

    quote do
      def unquote(String.to_atom "#{name}_def1")(arg) do
        IO.inspect "#{unquote(name)}_def1: #{arg}"
      end

      def unquote(String.to_atom "#{name}_def2")(arg) do
        IO.inspect "#{unquote(name)}_def2: #{arg}"
      end
    end
  end
end

defmodule UseSample do
  use Sample

  match :get, "path/to/hoge", "ActionModule", [alias_name: "hoge"]
end
Result:
iex> import UseSample
nil
iex> hoge_def1(1)
"hoge_def1: 1"
"hoge_def1: 1"
iex> hoge_def2(1)
"hoge_def2: 1"
"hoge_def2: 1"
関数として展開されている。
出力結果が変わらないよう注意しながら、もう少し手を加える。

Second step

第二段階のソースコードを記述します。
Example:
defmodule Sample do
  defmacro __using__(_options) do
    quote do
      Module.register_attribute __MODULE__, :routes, accumulate: true,
                                                     persist: false
      import unquote(__MODULE__)
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(env) do
    routes = Module.get_attribute(env.module, :routes)

    for route <- routes do
      {http_method, path, action, opts} = route
      quote do
        match(unquote(http_method), unquote(path), unquote(action), unquote(opts))
      end
    end
  end

  defmacro match(_http_method, _path, _action, opts) do
    name = opts[:alias_name]
    if name do
      quote do
        def unquote(String.to_atom "#{name}_def1")(arg) do
          IO.inspect "#{unquote(name)}_def1: #{arg}"
        end

        def unquote(String.to_atom "#{name}_def2")(arg) do
          IO.inspect "#{unquote(name)}_def2: #{arg}"
        end
      end
    end
  end

  defmacro get(path, action, opts) do
    add_route(:get, path, action, opts)
  end

  defp add_route(http_method, path, action, opts) do
    quote do
      @routes {unquote(http_method), unquote(path), unquote(action), unquote(opts)}
    end
  end
end

defmodule UseSample do
  use Sample

  get "path/to/hoge", "ActionModule", [alias_name: "hoge"]
  get "path/to/huge", "ActionModule", [alias_name: "huge"]
end
Result:
iex> import UseSample
nil
iex> hoge_def1(1)
"hoge_def1: 1"
"hoge_def1: 1"
iex> hoge_def2(2)
"hoge_def2: 2"
"hoge_def2: 2"
iex> huge_def1(1)
"huge_def1: 1"
"huge_def1: 1"
iex> huge_def2(2)
"huge_def2: 2"
"huge_def2: 2"
これで完成(?)です。
動作の流れについては次の項目。
(説明を全くしてなかったですね)

Brief description

簡単な動作の流れを記述します。
1. UseSampleモジュールで、Sampleモジュールをuseしている (Sample.using/1が展開される)
2. Sample.using/1では、アトリビュートの登録、インポート、Sample.before_compile/1の設定をしている
3. UseSampleモジュールで、Sample.get/3マクロを呼び出している
4. Sample.add_route/4では、アトリビュートの設定を行っている
5. Sample.before_compie/1では、アトリビュートの値を取得し、Sample.match/4を呼び出し引数として渡している
6. Sample.match/4は、alias_name:の値に応じた関数を展開している
簡単に流れを説明すると・・・こんな感じですね。
(説明足りなかったらコメント下さい)

Speaking to oneself

Phoenix-Framework v0.1.1のソースコードを参考にさせて頂きました。
参考にさせて頂いたソースコード(mapper.ex)ですが、最新だとソースコード自体が存在しませんでした。
同様の処理をどこかしらで行っているとは思いますが、
そこまで解読できていませんので・・・orz

Bibliography

Phoenix-Framework v0.1.1 - mapper.ex

人気の投稿