Goal
Elixirのマクロを展開してみる。
Dev-Environment
OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4
Wait a minute
マクロを展開するための手順をまとめる。
各関数の説明を見たい方は、公式のドキュメントかリンク先を見て下さい。
過去に簡単にまとめたものがあります。
過去に簡単にまとめたものがあります。
参考: マクロの簡単なまとめ
Index
Macro expand
|> Most simple example
|> Expansion only once
|> Recursively expanding
|> Extra
|> Most simple example
|> Expansion only once
|> Recursively expanding
|> Extra
Most simple example
おそらく、一番簡単な例。
Macro.to_string/2: ASTをバイナリに変換してくれる。
Example:
iex> expr = quote do: 1 + 1
{:+, [context: Elixir, import: Kernel], [1, 1]}
iex> Macro.to_string(expr)
"1 + 1"
でも、マクロの内部でマクロを呼んでいて、
さらに展開した先を見たい場合がある。
さらに展開した先を見たい場合がある。
Example:
例えばよく例に挙げられるunless。(私もあなたのお世話になります)
これを、そのままMacro.to_string/2してみる。
これを、そのままMacro.to_string/2してみる。
iex> expr = quote do: unless(true, do: false, else: true)
{:unless, [context: Elixir, import: Kernel], [true, [do: false, else: true]]}
iex> Macro.to_string(expr)
"unless(true) do\n false\nelse\n true\nend"
unlessマクロの中では別のマクロを呼んでいる。
しかし、これだと見ることができない。
しかし、これだと見ることができない。
その場合はどうすればいいんだろうか?
Expansion only once
ASTを展開するには、この関数を使えばできる。
Macro.expand_once/2: ASTを一度だけ展開してくれる。
Example:
unlessを展開してみる。
iex> expr = quote do: unless(true, do: false, else: true)
{:unless, [context: Elixir, import: Kernel], [true, [do: false, else: true]]}
iex> Macro.expand_once(expr, __ENV__)
{:if, [context: Kernel, import: Kernel], [true, [do: true, else: false]]}
iex> Macro.expand_once(expr, __ENV__) |> Macro.to_string
"if(true) do\n true\nelse\n false\nend"
unlessを展開するとifになってますね。
つまり・・・unless → ifとなっているようですね。
つまり・・・unless → ifとなっているようですね。
Recursively expanding
上記の関数だと展開は一回だけです。
さらに、その先の展開が必要な時はこちらを使いましょう。
さらに、その先の展開が必要な時はこちらを使いましょう。
Macro.expand/2: ASTを再帰的に展開してくれます。
Example:
同じく、unlessを展開してみる。
iex> expr = quote do: unless(true, do: false, else: true)
{:unless, [context: Elixir, import: Kernel], [true, [do: false, else: true]]}
iex> Macro.expand(expr, __ENV__)
{:case, [optimize_boolean: true],
[true,
[do: [{:->, [],
[[{:when, [],
[{:x, [counter: 12], Kernel},
{:in, [context: Kernel, import: Kernel],
[{:x, [counter: 12], Kernel}, [false, nil]]}]}], false]},
{:->, [], [[{:_, [], Kernel}], true]}]]]}
iex> Macro.expand(expr, __ENV__) |> Macro.to_string
"case(true) do\n x when x in [false, nil] ->\n false\n _ ->\n true\nend"
unlessは最終的にcaseになってますね。
つまり・・・unless → if → caseとなっているようですね。
つまり・・・unless → if → caseとなっているようですね。
Extra
unlessのソースコードを読んでみる。
※ 公式のソースコードからコピペで引用しています。
引用: Github - v1.0.5 Elixir - Kernel.unless/2
defmacro unless(clause, options) do
do_clause = Keyword.get(options, :do, nil)
else_clause = Keyword.get(options, :else, nil)
quote do
if(unquote(clause), do: unquote(else_clause), else: unquote(do_clause))
end
end
以下を例に展開してみる。
unless(true, do: false, else: true)
第一引数(clause): true
第二引数(options): キーワードリスト(do: and else:)
第二引数(options): キーワードリスト(do: and else:)
キーワードリストの値の部分をそれぞれ取得し、
ifマクロを呼び出してdo:とelse:部分を逆に渡している。
ifマクロを呼び出してdo:とelse:部分を逆に渡している。
引用: Github - v1.0.5 Elixir - Kernel.if/2
defmacro if(condition, clauses) do
do_clause = Keyword.get(clauses, :do, nil)
else_clause = Keyword.get(clauses, :else, nil)
optimize_boolean(quote do
case unquote(condition) do
x when x in [false, nil] -> unquote(else_clause)
_ -> unquote(do_clause)
end
end)
end
do:、else:の下りは同じ。
そこから、caseにしてガードを使ってマッチングしているみたいですね。
第一引数(条件)の部分がfalseかnilならelse:を実行している。
そうでなければ、do:を実行している。
第一引数(条件)の部分がfalseかnilならelse:を実行している。
そうでなければ、do:を実行している。
といったところでしょう。
optimeze_booleanは知らん!
(何だよ真偽値の最適化って・・・)
(何だよ真偽値の最適化って・・・)
引用: Github - v1.0.5 Elixir - Kernel.case/2
defmacro case(condition, clauses)
さらにcaseの実装を見ようと思ったのですが・・・書いてないだと!?
見るところ間違えたのでしょうか?caseの実装・・・どこに書いてあるんだろう?
見るところ間違えたのでしょうか?caseの実装・・・どこに書いてあるんだろう?
といったところで、締まらないですが終わりです。
unlessって以下のような書き方もできますが・・・
これが動作する理由が分からない。
これが動作する理由が分からない。
unless true do
false
else
true
end
今の私には無理ってことですね。
はい、あきらめます。
はい、あきらめます。
Speaking to oneself
中々・・・すごいですね・・・メタプログラミング・・・恐ろしい子だ。
展開手順と追い方的にはこんなところでしょうか?
これが足りない、ここが分からんと意見のある方は一報頂けると嬉しいです。
これが足りない、ここが分からんと意見のある方は一報頂けると嬉しいです。
しかしこの記事、おまけが本番な気がする。