技術メモ

プログラミングとか電子工作とか

Elixir入門(Meta-programming編 第1章 Quote and unquote)

f:id:ysmn_deus:20190122112104p:plain

どうも、靖宗です。
Mix and OTP編も終わりましたので、今回からはMeta-programming編となります。
おそらく詳細な内部実装などの話になってくるので、今まで謎だった挙動とかが明らかになると信じて進めて行きます。
(マクロなどもこの分野だそうです)
このMeta-programming編の最終目標は独自のDSLを作成することだそうですが、あんまりソフトウェア工学の知識が無いのでなんのことやら・・・
とりあえずサンプルに沿って進めます。

Quoting

Mix and OTP編 第10章でも若干触れましたが、Elixirの中で関数は実際にはタプルのような形式で取り扱われているようです。
例えば、sum(1, 2, 3)という関数は

{:sum, [], [1, 2, 3]}

というデータとして取り扱われているそうです。
この内部実装されている実際の形式を取得するにはquoteというマクロを利用するそうです。

iex> quote do: sum(1, 2, 3)
{:sum, [], [1, 2, 3]}

タプルの最初の要素は関数の名前、二個目の要素はメタデータの要素、三個目の要素が引数となっています。
メタデータに関しては?ですがそのうち分かってくることと信じて進みます。

演算子+-など)も内部実装された形式を見ることができます。

iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}

:+という名前の関数で、[context: Elixir, import: Kernel]というメタデータ(Elixirのカーネルから呼び出される関数という情報?)を持っており、引数が[1, 2]であることが分かります。
ただの足し算や引き算も関数として実装されていることが分かりました。

マップに関しても内部実装を見てみると、関数として実装されていることが分かります。

iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}

変数も似たようなタプルで表現されています。

iex> quote do: x
{:x, [], Elixir} # 最後のElixirはElixir.stringなどのモジュール名だと思われるので、おそらくアトム

複雑な表現であっても、Elixir内では必ずこのようなタプルで表現されているので、上記のようなタプルがネストしたツリーが見れるはずです。多くの言語ではこういう構造を抽象構文木(Abstract Syntax Tree、AST)と呼ぶそうですが、Elixirではquoted expressionと呼ぶそうです。

iex> quote do: sum(1, 2 + 3, 4)
{:sum, [], [1, {:+, [context: Elixir, import: Kernel], [2, 3]}, 4]}

純粋にネストした構造になっています。

quoted expressionsを普通の関数っぽくなおす関数も用意されており、Macro.to_string/1で普通のコードっぽく表現できます。。

iex> Macro.to_string(quote do: sum(1, 2 + 3, 4))
"sum(1, 2 + 3, 4)"

上記はつまり

iex> Macro.to_string({:sum, [], [1, {:+, [context: Elixir, import: Kernel], [2, 3]}, 4]})
"sum(1, 2 + 3, 4)"

となっている訳です。

一般的に、上記までのタプルは下記のフォーマットで構成されているようです。

{atom | tuple, list, list | atom}
  • 最初の要素はアトムか、同じ様なタプル(ネストしてる)
  • 二個目の要素はコンテキストなどのメタデータをもっているキーワードリスト
  • 三個目の要素は関数であれば引数、そうでなければ(変数などのとき)アトム。

Elixirでは上記の3つの要素を持つタプルか、または下記の5つの要素いずれかの要素で内部実装されているようです。

:sum         #=> Atoms
1.0          #=> Numbers
[1, 2]       #=> Lists
"strings"    #=> Strings
{key, value} #=> Tuples with two elements

全ての要素は上記5つのなにがしか+3要素のタプルで必ず表現されることになります。
ちなみに、3個以上の普通のタプルは{:{}, [], タプルの要素のリスト}として内部実装されます。

他にも五章で学んだcase構文をquoteに放り込んでみます。

iex(18)> x = 1
iex(19)> quote do
...(19)>   case 10 do
...(19)>     ^x -> "Won't match"
...(19)>     _ -> "Will match"
...(19)>   end
...(19)> end
{:case, [],
 [
   10,
   [
     do: [
       {:->, [], [[{:^, [], [{:x, [], Elixir}]}], "Won't match"]},
       {:->, [], [[{:_, [], Elixir}], "Will match"]}
     ]
   ]
 ]}

かなり複雑ですが、きちんと6要素で表されていることが分かります。

{:case, [], A} # 3連タプル
A: [10, B] # リスト
B: [do: C] # キーワードリスト(キーワードリストは実質的には2個のタプルのリスト)
C: [D, {:->, [], [[{:_, [], Elixir}], "Will match"]}] # 3連タプルのリスト、2個目は一個目同様
D: {:->, [], E} # 3連タプル 
E: [F, "Won't match"] # リスト
F: [ {:^, [], [G]} ] # 3連タプルのリスト
G: {:x, [], Elixir} # 3連タプル

キーワードリストは[{key, value}, {key, value}]という実装なので2連タプルのリストです。

Unquoting

quoteは内部実装を6要素の内部実装で表現するマクロでした。
ここで、変数を使った式をquoteしてMacro.to_string/1した場合はどうなるでしょうか。

iex> number = 13
iex> Macro.to_string(quote do: 11 + number)
"11 + number"

numberが代入されないまま式に変形されてしまいました。
別にええやんという気もしないでもないんですが、メタプログラミングする上で「ここでは変数は代入されて実数が入って欲しい」みたいな場合はunquoteというマクロを使うそうです。

iex> number = 13
iex> Macro.to_string(quote do: 11 + unquote(number))
"11 + 13"

unquoteは関数の名前を代入することもできます。

iex> fun = :hello # Elixirの内部では関数名は全てアトム
iex> Macro.to_string(quote do: unquote(fun)(:world))
"hello(:world)"

他にも、リストを代入したいときに要素を分解して入れたいとき等があると思います。
文で説明するのはヤヤコシイのでサンプルを見て下さい。

iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote(inner), 6])
"[1, 2, [3, 4, 5], 6]" # ホントは[1, 2, 3, 4, 5, 6]としたい!

いわゆるJavaScriptのSpread operatorのような代入をしたいときがおそらくあるのでしょう。
こういうときはunquote_splicingというマクロを利用することで解決できるそうです。

iex> inner = [3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote_splicing(inner), 6])
"[1, 2, 3, 4, 5, 6]"

割とメタプログラミングでは重宝するかもしれません。

Escaping

quoted expressionの6つの要素で表現されている変数に関しては上記のunquoteで代入できますが、そうでない場合は正しく代入されません。
試しにmapをunquoteで代入しようとしてみます。

iex(1)> map = %{hello: :world}
%{hello: :world}
iex(2)> quote do: unquote(map)
%{hello: :world}

これはquoted expressionsとなっていません。
おそらくコレはコンパイル時のマクロの挙動で代入する順番が逐次処理されるような形で実装されているので、最後にunquoteが評価されてしまっているのでしょう。
(unquote(map)が先に評価されてquoteしてくれれば正しい動作になるはず)
そこで、quoted expressionの6つの要素以外を代入するときにはMacro.escape/1を利用します。

iex> Macro.escape(map)
{:%{}, [], [hello: :world]}

quoted expressionと通常のElixirの表現が入り交じって頭がこんがらがりそうですが、丁寧にひもといていけば問題無いかとおもいます。
次回はマクロを作成していきます。
上記を踏まえると引数をquoteして内部実装の状態で加工してMacro.to_stringする感じでしょうか。