Goal
マクロの応用形を習得する。
Dev-Environment
OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.5
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
|> 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を返す。
Example:
- 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における一部分を実装します。)
その後、これを使ったマクロの実装を今回は紹介していきます。
(Ecto.Model.Callbacksにおける一部分を実装します。)
Macro pattern
それでは、マクロの応用パターンをまとめていきます。
と言っても、以前同様の内容をやっています。
と言っても、以前同様の内容をやっています。
覚えていらっしゃる方はいるでしょうか?
記事: Macro to expand the function
記事: Macro to expand the function
読んだソースコードはあまり多くはないですが、
それでも割と目にする機会が多いマクロの使い方だったので、今回説明も付けてまとめます。
(以前の記事での説明は…おざなりでしたね。すいません。)
それでも割と目にする機会が多いマクロの使い方だったので、今回説明も付けてまとめます。
(以前の記事での説明は…おざなりでしたね。すいません。)
ソースコードは以前の記事から拝借して、始めていきましょう。
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)
Sample.get/3 -> Sample.add_route/4 -> アトリビュートへ値を束縛(routes)
次、UseSampleでusingを展開している部分。
use Sample -> アトリビュートの登録(routes) -> before_compile -> アトリビュートの取得 -> Sample.match/4 -> 関数が展開
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(のようなもの)を実装してみます。
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と言ったように実行して欲しい。
Sample.insert/0を実行すると、before_insert -> insert -> after_insertと言ったように実行して欲しい。
作成するモジュールは3つ。
- Callbacks: コールバックを扱うモジュール (マクロ)
- DB_Accesser: dbへの仮アクセスを行うはずのモジュール
- User: 上記二つを利用するモジュール (モデル)
Caution:
当り前ですが、実際のEctoではもっと洗練されたソースコードが実装されています。
必要な部分を抜き出して実装しているので、予めご了承下さい。
必要な部分を抜き出して実装しているので、予めご了承下さい。
では、実装していきましょう。
第一段階
Userモジュールに定義してある関数をDB_Accesserモジュールから呼び出す。
呼び出し方は、上の方で使ったfunction_exported?/3、apply/3を利用しています。
呼び出し方は、上の方で使った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したモジュールで展開するようにしました。
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は抜いています)
(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マクロを実装して終わりにします。
最後に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(1)> 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