スポンサーリンク

2015年8月27日

[Elixir]マクロをくぱぁ(展開)する手順

とある錬金術師の万能薬(Elixir)

Goal

Elixirのマクロを展開してみる。

Dev-Environment

OS: Windows8.1
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

おそらく、一番簡単な例。
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してみる。
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となっているようですね。

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となっているようですね。

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:)
キーワードリストの値の部分をそれぞれ取得し、
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:を実行している。
といったところでしょう。
optimeze_booleanは知らん!
(何だよ真偽値の最適化って・・・)

引用: Github - v1.0.5 Elixir - Kernel.case/2

defmacro case(condition, clauses)
さらにcaseの実装を見ようと思ったのですが・・・書いてないだと!?
見るところ間違えたのでしょうか?caseの実装・・・どこに書いてあるんだろう?
といったところで、締まらないですが終わりです。
unlessって以下のような書き方もできますが・・・
これが動作する理由が分からない。
unless true do
  false
else
  true
end
今の私には無理ってことですね。
はい、あきらめます。

Speaking to oneself

中々・・・すごいですね・・・メタプログラミング・・・恐ろしい子だ。
展開手順と追い方的にはこんなところでしょうか?
これが足りない、ここが分からんと意見のある方は一報頂けると嬉しいです。
しかしこの記事、おまけが本番な気がする。

Bibliography

人気の投稿