LoginSignup
29
25

More than 5 years have passed since last update.

[Elixir]callbackマクロを実装する

Last updated at Posted at 2015-09-10

現在、Ectoではcallbackマクロはなくなったとのことです。
そのため、マクロの一例としてご覧ください。

Goal

マクロの応用パターンを習得する。

Dev-Environment

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

Wait a minute

マクロで作成されているソースコードでよく見る形をまとめます。

最初にTipsをやります。
内容的に目的が二つ以上になってしまうので、あまり良くはないのですが、
覚えておいた方が進行がスムーズになるので、一緒にやってしまいます。

そんなの知ってるよ!って方は、読み飛ばして下さい。

Index

Implement a callback macro
|> Map.update/4
|> Kernel.function_exported?/3 & Kernel.apply/3
|> Macro pattern
|> Implement a callback macro
|> Note

Map.update/4

Map.update/4の動作がいまいち分かり辛かったのでまとめてます。

Example:

iex> map = %{hoge: "hoge", huge: "huge"}
%{hoge: "hoge", huge: "huge"}
  • 指定したキーがあれば、第三引数の初期値は動作しない (その場合、第四引数は実行される)
iex> Map.update(map, :hoge, "foo", &(&1 <> "aaa"))
%{hoge: "hogeaaa", huge: "huge"}
  • 指定したキーがなければ、第三引数の初期値が動作する (その場合、第四引数が実行されない)
iex> Map.update(map, :foo, "foo", &(&1 <> "aaa"))
%{foo: "foo", hoge: "hoge", huge: "huge"}

使いどころが限定的な気もしないでもない関数ですね...

Kernel.function_exported?/3 & Kernel.apply/3

モジュールに関数が存在するか判定する関数(function_exported?/3)とモジュールの関数を実行する関数(apply/3)です。

使い方。

Example

  • function_exported?/3
iex> function_exported?(Enum, :reverse, 1)
true
iex> function_exported?(Enum, :reverse, 2)
true
iex> function_exported?(Enum, :reverse, 3)
false
iex> function_exported?(Enum, :hoge, 1)
false

Note:
第一引数: モジュール
第二引数: 関数名
第三引数: アリティ

存在すれば、true。そうでなければ、falseを返す。

  • apply/3
iex> apply(Enum, :reverse, [[1,2,3]])
[3, 2, 1]
iex> apply(Enum, :reverse, [1,2,3])
** (UndefinedFunctionError) undefined function: Enum.reverse/3
    (elixir) Enum.reverse(1, 2, 3)
iex> apply(Enum, :join, [[1,2,3], "+"])
"1+2+3"

Note:
第一引数: モジュール
第二引数: 関数名
第三引数: 第二引数の関数へ渡す引数

  • 組み合わせてみる
iex> if function_exported?(Enum, :reverse, 1) do
...>   apply(Enum, :reverse, [[1,2,3]])
...> end
[3, 2, 1]

何ができるのか?
標準関数だと有難味があまり感じられません。

ですが、自分で定義しているモジュールならどうでしょうか?

Example:

defmodule Sample do
  def test(hoge) do
    IO.inspect hoge
  end
end

Result:

iex> if function_exported?(Sample, :test, 1) do
...>   apply(Sample, :test, [1])
...> end
1
1

特定の関数名を持っているモジュールの関数を判定して実行させることができます。
これは、とても良い。マクロと組み合わせると非常に良い(メタプロの暗黒面?)

前置きはここまで...マクロの応用パターンをまとめます。
その後、これを使ったマクロの実装を今回は紹介していきます。
(Ecto.Model.Callbacksにおける一部分を実装します。)

Macro pattern

それでは、マクロの応用パターンをまとめていきます。
と言っても、以前同様の内容をやっています。

覚えていらっしゃる方はいるでしょうか?
記事: マクロで関数を展開する一例

読んだソースコードはあまり多くはないですが、
それでも割と目にする機会が多いマクロの使い方だったので、今回説明も付けてまとめます。
(以前の記事での説明は...おざなりでしたね。すいません。)

ソースコードは以前の記事から拝借して、始めていきましょう。

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

処理の流れ...二つの流れがあるため、少し分かりずらいと思います。

まず、UseSampleで記述しているget/3の部分。
Sample.get/3 -> Sample.add_route/4 -> アトリビュートへ値を束縛(routes)

次、UseSampleでusingを展開している部分。
use Sample -> アトリビュートの登録(routes) -> before_compile -> アトリビュートの取得 -> Sample.match/4 -> 関数が展開

こんな順番で実行されている。
重要なのはアトリビュートの部分。

using部分は、use Sampleを展開した時に実行されるが、
アトリビュートの登録をしたかのように、モジュール内のマクロや関数でアトリビュートを利用することができる。

慣れれば、大したことはないのだが、
慣れないうちは、処理の流れが二つあるように感じるので、分かりにくいと思う。

しかし、結局は展開された最終的なソースコードになるだけです。

無理に二つ以上追わず、一つ一つ潰していくこと。

Note:

アトリビュートの値ですが、同じアトリビュートへ束縛しようとすると、値が蓄積していきます。
新しいアトリビュートの値は、常にリストの先頭に追加されます。

こんな感じに...

defmodule Sample do
  Module.register_attribute __MODULE__,
    :sample, accumulate: true, persist: false

  @sample 10
  @sample 20
  @sample #=> [20, 10]
end

ゆえに、for記述などで繰り返し処理できるわけですね。

Implement a callback macro

応用パターンを理解したところで、これを使った実践的な実装を行ってみましょう。
Ecto.Model.Callbacks(のようなもの)を実装してみます。

こんな風に書きたい。

Example:

defmodule Sample do
  use Callbacks

  before_insert :before_execution
  after_insert :after_execution

  def insert do
    DB_Accesser.insert(Sample)
  end

  # implement before_execution

  # implement after_execution
end

処理の流れとしては、
Sample.insert/0を実行すると、before_insert -> insert -> after_insertと言ったように実行して欲しい。

作成するモジュールは3つ。

  • Callbacks: コールバックを扱うモジュール (マクロ)
  • DB_Accesser: dbへの仮アクセスを行うはずのモジュール
  • User: 上記二つを利用するモジュール (モデル)

Caution:
当り前ですが、実際のEctoではもっと洗練されたソースコードが実装されています。
必要な部分を抜き出して実装しているので、予めご了承下さい。

では、実装していきましょう。

第一段階

Userモジュールに定義してある関数をDB_Accesserモジュールから呼び出す。
呼び出し方は、上の方で使ったfunction_exported?/3、apply/3を利用しています。

まだ、Callbacksモジュールはありません。

Example:

defmodule DB_Accesser do
  def insert(module, string) do
    if function_exported?(module, :before_insert, 1) do
      apply(module, :before_insert, [string])
    end
  end
end

defmodule User do
  def insert do
    DB_Accesser.insert(__MODULE__, "hogehoge")
  end

  def before_insert(string) do
    before_execution(string)
  end

  def before_execution(string) do
    IO.puts string
  end
end

Result:

iex> User.insert
hogehoge
:ok

以降、実行結果が変更されるまで結果は記述しません。

第二段階

Callbacksモジュールを追加します。
before_insert/1のコールバック関数は、Callbacksをuseしたモジュールで展開するようにしました。

Example:

defmodule Callbacks do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(_env) do
    quote do
      def before_insert(string) do
        before_execution(string)
      end
    end
  end
end

...

defmodule User do
  use Callbacks

  def insert do
    DB_Accesser.insert(__MODULE__, "hogehoge")
  end

  def before_execution(string) do
    IO.puts string
  end
end

第三段階

before_insert/1のコールバック関数をマクロを使って展開します。

Example:

defmodule Callbacks do
  ...

  defmacro __before_compile__(_env) do
    event = :before_insert
    function = :before_execution

    quote do
      def unquote(event)(string) do
        unquote(function)(string)
      end
    end
  end
end

第四段階

before_insert/1マクロとregister_callback/2を作成しました。
また、アトリビュートを追加しました。

実装しただけなので、まだ使えません。

Example:

defmodule Callbacks do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)
      @before_compile unquote(__MODULE__)
      @callbacks %{}
    end
  end

  defmacro __before_compile__(_env) do
    callbacks = Module.get_attribute(env.module, :callbacks)
    event = :before_insert
    function = :before_execution

    quote do
      def unquote(event)(string) do
        unquote(function)(string)
      end
    end
  end

  defmacro before_insert(function) do
    register_callback(:before_insert, function)
  end

  defp register_callback(event, function) do
    @callbacks Map.update(@callbacks, event, [function], &[function|&1])
  end
end

Example:

Map.update/4の内容がどうなっているのか、
分かり辛いので仮データを使って出力させます。

iex> map = %{}
%{}
iex> function = :before_execution
:before_execution
iex> map = Map.update(map, :before_insert, [function], &[function|&1])
%{before_insert: [:before_execution]}
iex> function = :before_execution2
:before_execution2
iex> map = Map.update(map, :before_insert, [function], &[function|&1])
%{before_insert: [:before_execution2, :before_execution]}

第五段階

アトリビュートの値からコールバック関数を作成します。
やってることは単純なんですが、ここが一番難解だと思います。

Example:

まず、iex上で作成する予定の処理をquoteしてどう展開されるのか確認します。

iex> quoted = quote do
...>   for {event, callback} <- %{before_insert: [:before_execution2, :before_execution]} do
...>     Enum.reduce(Enum.reverse(callback),
...>       quote(do: string), fn(function, acc) -> quote(do: unquote(function)(unquote(acc))) end)
...>   end
...> end
{:for, [],
 [{:<-, [],
   [{{:event, [], Elixir}, {:callback, [], Elixir}},
    {:%{}, [], [before_insert: [:before_execution2, :before_execution]]}]},
  [do: {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :reduce]}, [],
    [{{:., [], [{:__aliases__, [alias: false], [:Enum]}, :reverse]}, [],
      [{:callback, [], Elixir}]}, {:quote, [], [[do: {:string, [], Elixir}]]},
     {:fn, [],
      [{:->, [],
        [[{:function, [], Elixir}, {:acc, [], Elixir}],
         {:quote, [],
          [[do: {{:unquote, [], [{:function, [], Elixir}]}, [],
             [{:unquote, [], [{:acc, [], Elixir}]}]}]]}]}]}]}]]}
iex> Macro.expand(quoted, __ENV__) |> Macro.to_string |> IO.puts
for({event, callback} <- %{before_insert: [:before_execution2, :before_execution]}) do
  Enum.reduce(Enum.reverse(callback), quote() do
    string
  end, fn function, acc -> quote() do
    unquote(function)(unquote(acc))
  end end)
end
:ok

Example:

問題ないようなので実装します。

defmodule Callbacks do
  ...

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

    for {event, callback} <- callbacks do
      body = Enum.reduce(Enum.reverse(callback),
                         quote(do: string),
                         fn(function, acc) -> quote(do: unquote(function)(unquote(acc))) end)

      quote do
        def unquote(event)(string) do
          unquote(body)
        end
      end
    end
  end

  ...
end

...

defmodule User do
  use Callbacks

  before_insert :before_execution

  ...
end

Example:

Enum.reduce/3の部分が分かり辛いと思いますので、分解して見てみましょう。
(Enum.reverse/1は抜いています)

iex> body = Enum.reduce([:before_execution2, :before_execution], quote(do: string), fn(function, acc) -> quote(do: unquote(function)(unquote(acc))) end)
{:before_execution, [], [{:before_execution2, [], [{:string, [], Elixir}]}]}
iex> Macro.to_string(quote(do: unquote(body)))
"before_execution(before_execution2(string))"

関数の戻り値を引数として次の関数を実行するように展開していますね。

このことから分かりますが、戻り値に引数の値を返してあげないと、
二つ以上の動作をさせる場合、不具合が起こります。

コールバックでchangesetを戻り値とするのは、こういった実装だったからみたいですね。

第六段階

二つのコールバックを定義して、動作させてみます。

Example:

defmodule User do
  use Callbacks

  before_insert :before_execution
  before_insert :before_execution2

  ...

  def before_execution(string) do
    IO.puts "-- before_execution --"
    IO.puts string
    string
  end

  def before_execution2(string) do
    IO.puts "-- before_execution2 --"
    IO.puts string
    string
  end
end

Result:

iex> User.insert
-- before_execution --
hogehoge
-- before_execution2 --
hogehoge
"hogehoge"

問題なく動作しますね。

第七段階

まだ修正できる部分は多々ありますが、
最後にafter_insertマクロを実装して終わりにします。

Example:

defmodule Callbacks do
  ...

  defmacro after_insert(function) do
    register_callback(:after_insert, function)
  end

  ...
end

defmodule DB_Accesser do
  def insert(module, string) do
    if function_exported?(module, :before_insert, 1) do
      apply(module, :before_insert, [string])
    end

    # DB insert processing

    if function_exported?(module, :after_insert, 1) do
      apply(module, :after_insert, [string])
    end
  end
end

defmodule User do
  use Callbacks

  before_insert :before_execution
  before_insert :before_execution2
  after_insert :after_execution

  ...

  def before_execution(string) do
    IO.puts "-- before_execution --"
    IO.puts string
    string
  end

  def before_execution2(string) do
    IO.puts "-- before_execution2 --"
    IO.puts string
    string
  end

  def after_execution(string) do
    IO.puts "-- after_execution --"
    IO.puts string
    string
  end
end

Result:

iex> User.insert
-- before_execution --
hogehoge
-- before_execution2 --
hogehoge
-- after_execution --
hogehoge
"hogehoge"

動作も問題なし。

完成形

少し修正した部分などがあります。

defmodule Callbacks do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)
      @before_compile unquote(__MODULE__)
      @callbacks %{}
    end
  end

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

    for {event, callback} <- callbacks do
      body = Enum.reduce(Enum.reverse(callback),
                         quote(do: string),
                         &compile_callback/2)

      quote do
        def unquote(event)(string) do
          unquote(body)
        end
      end
    end
  end

  defmacro before_insert(function) do
    register_callback(:before_insert, function)
  end

  defmacro after_insert(function) do
    register_callback(:after_insert, function)
  end

  defp compile_callback(function, acc) when is_atom(function) do
    quote do
      unquote(function)(unquote(acc))
    end
  end

  defp register_callback(event, function) do
    quote do
      @callbacks Map.update(@callbacks, unquote(event), [unquote(function)], &[unquote(function)|&1])
    end
  end

  def __apply__(module, callback, string) do
    if function_exported?(module, callback, 1) do
      apply(module, callback, [string])
    end
  end
end

defmodule DB_Accesser do
  require Callbacks

  def insert(module, string) do
    # before processing
    Callbacks.__apply__(module, :before_insert, string)

    # DB insert processing

    # after processing
    Callbacks.__apply__(module, :after_insert, string)
  end
end

defmodule User do
  use Callbacks

  before_insert :before_execution
  before_insert :before_execution2
  after_insert :after_execution

  def insert do
    DB_Accesser.insert(__MODULE__, "hogehoge")
  end

  def before_execution(string) do
    IO.puts "-- before_execution --"
    IO.puts string
    string
  end

  def before_execution2(string) do
    IO.puts "-- before_execution2 --"
    IO.puts string
    string
  end

  def after_execution(string) do
    IO.puts "-- after_execution --"
    IO.puts string
    string
  end
end

Note

マクロに関しての考察。

マクロを展開する際の注意点として...

一つ目に、関数が間に入ると、その先のマクロ展開をしてくれない。
(マクロ -> 関数 -> マクロのような状態)

これは意外と面倒なんです。

追うのは、そんなに難しくないですが、
展開後のソースコードを知りたい時、展開しても途中で止まるので手動で展開していかなければいけません。
引数の値を用意するのも結構、面倒ですね。

二つ目、モジュールのアトリビュートに登録がある場合、before_compileなどで処理をしている場合がある。
つまり、マクロの終端でアトリビュートへ値を束縛している可能性がある。

こちらは、終端まで行き着いてしまえば、アトリビュート名で検索して一発です。
そこで終わりにならない場合があると言った認識があれば問題ないと思います。

この二点に注意を払ってマクロを追跡すると良いかと思います。

また、マクロを作る時は完成形を先に考えて展開していった方が良いと思う。
実際に使う形から、展開していくようにしてパーツを作っていった方が上流から流れていく自然な流れになると思います。
(これはあくまで個人的感覚なので、ご自身のやりやすいようにして下さい。)

こんなところですね。

Speaking to oneself

Elixirのマクロはよくできてますね。
メタプログラミングをここまで学習したのは初めてなので、驚きの連続でした。

他の言語でもこんな感じなのでしょうか?(無知)

マクロの記事はこれで一旦終了です。
また気が向いたら書くと思います。

女心と秋の空じゃないですが、かなり移り気な人間なので、
割と近い内に書くと思いますが(笑)

マクロをやっているとプログラムをしている気になれる!

というわけで、皆さんもマクロを使いまくってみましょう!
凄腕のプログラマになった気分が味わえます(笑)

それではこの辺でm(_ _)m

Bibliography

github - Ecto - ecto/lib/ecto/model/callbacks.ex
github - Ecto - ecto/lib/ecto/repo/model.ex
hexdocs - Elixir v1.0.5 - Enum
hexdocs - Elixir v1.0.5 - Map.update/4
hexdocs - Elixir v1.0.5 - Kernel.function_exported/3
hexdocs - Elixir v1.0.5 - Kernel.apply/3

29
25
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
29
25