こちらも再確認に着手。
quote do
で AST が確認できる、というのがアレゲです。
iex(4)> quote do: div(10,2)
{:div, [context: Elixir, import: Kernel], [10, 2]}
iex(5)> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}
あと、if
がマクロ、というのもナニ (何
unless を作る
コードがコピペできないのは微妙に辛い。macros/unless.exs が以下。
defmodule ControlFlow do
defmacro unless(expression, do: block) do
quote do
if !unquote(expression), do: unquote(block)
end
end
end
iex 起動して以下。
iex(1)> c "unless.exs"
[ControlFlow]
iex(2)> require ControlFlow
ControlFlow
iex(3)> ControlFlow.unless 2 == 5, do: "block entered"
"block entered"
iex(4)> ControlFlow.unless 5 == 5, do: "block entered"
nil
iex(5)> ControlFlow.unless 5 == 5 do
...(5)> "block entered"
...(5)> end
nil
マクロの場合、require しないと駄目みたい? 試してみます。
iex(1)> c "unless.exs"
[ControlFlow]
iex(2)> ControlFlow.unless 2 == 5, do: "block entered"
** (CompileError) iex:2: you must require ControlFlow before invoking the macro ControlFlow.unless/2
(elixir) src/elixir_dispatch.erl:97: :elixir_dispatch.dispatch_require/6
そうですね。
マクロの目的
AST 式を取り込んで AST 式を戻す、とあります。たしかに上の unless の定義はそんな記述に見えるんですが、unquote て何かと。
unquote?
Math.say で最初に出てきたよね、とか書いてますね。ちょっと巻き戻してみます。以下な定義の記述がありました。スルーしてましたw
defmacro say({:+, _, [lhs, rhs]}) do
quote do
lhs = unquote(lhs)
rhs = unquote(rhs)
result = lhs + rhs
IO.puts "#{lhs} plus #{rhs} is #{result}"
result
end
end
eval/apply なナニ、という理解で良いのかどうか。そして確かに上の目的な記載通りの動作になっていることがわかります。
Code.eval_quoted
AST を eval する命令があるとのこと。試せ、とあるので試してみます。
iex(2)> number = 5
5
iex(3)> ast = quote do
...(3)> number * 10
...(3)> end
{:*, [context: Elixir, import: Kernel], [{:number, [], Elixir}, 10]}
iex(4)> Code.eval_quoted ast
warning: variable "number" does not exist and is being expanded to "number()", please use parentheses to remove the ambiguity or change the variable name
nofile:1
** (CompileError) nofile:1: undefined function number/0
(stdlib) lists.erl:1354: :lists.mapfoldl/3
(elixir) lib/code.ex:590: Code.eval_quoted/3
iex(4)> ast = quote do
...(4)> unquote(number) * 10
...(4)> end
{:*, [context: Elixir, import: Kernel], [5, 10]}
iex(5)> Code.eval_quoted ast
{50, []}
変数は unquote しないと AST の中で値として扱うことができない、という理解で良いのかどうか。say の定義も同様ですね。あるいは unless の定義も同様。
マクロの展開
コンパイラはマクロ展開において、マクロでなくなるまでカワを剥いでいく模様。例示されているナニを確認してみます。
iex(1)> c "unless.exs"
[ControlFlow]
iex(2)> require ControlFlow
ControlFlow
iex(3)> ast = quote do
...(3)> ControlFlow.unless 2 == 5, do: "block entered"
...(3)> end
{{:., [], [{:__aliases__, [alias: false], [:ControlFlow]}, :unless]}, [],
[{:==, [context: Elixir, import: Kernel], [2, 5]}, [do: "block entered"]]}
iex(4)> expanded_once = Macro.expand_once(ast, __ENV__)
{:if, [context: ControlFlow, import: Kernel],
[
{:!, [context: ControlFlow, import: Kernel],
[{:==, [context: Elixir, import: Kernel], [2, 5]}]},
[do: "block entered"]
]}
ここまでで一つカワを剥いだ状態ですね。if はまだマクロです。
iex(5)> expand_fully = Macro.expand_once(expanded_once, __ENV__)
{:case, [optimize_boolean: true],
[
{:!, [context: ControlFlow, import: Kernel],
[{:==, [context: Elixir, import: Kernel], [2, 5]}]},
[
do: [
{:->, [],
[
[
{:when, [],
[
{:x, [counter: -576460752303423453], Kernel},
{{:., [], [Kernel, :in]}, [],
[{:x, [counter: -576460752303423453], Kernel}, [false, nil]]}
]}
],
nil
]},
{:->, [], [[{:_, [], Kernel}], "block entered"]}
]
]
]}