/var/log/messages

Feb 10, 2019 - 2 minute read - Comments - programming

Metaprogramming Elixir (1)

こちらも再確認に着手。

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"]}
     ]
   ]
 ]}

プログラミング Elixir (17) Metaprogramming Elixir (2)

comments powered by Disqus