本稿はElixir公式サイトの許諾を得て「Quote and unquote」の解説にもとづき、加筆補正を加えて、Elixirで使えるメタプログラミング技術のうち内部表現の扱いについてご説明します。Elixirプログラムを独自のデータ構造で表す機能は、メタプログラミングの基本です。そのデータ構造は内部表現(quoted expression)と呼ばれます。
quote/2による式の表現
Elixirのプログラムで式は、3要素のタプルで組み立てられます。 式をタプルで返すのがマクロquote/2
です。たとえば、関数sum(1, 2, 3)の呼び出しは、つぎのタプルで表されます。
iex> quote do: sum(1, 2, 3)
{:sum, [], [1, 2, 3]}
タプルの第1要素は関数名、第2要素がメタデータを含むリスト、そして第3要素には引数がリストで示されます。
関数だけでなく、演算子も3要素のタプルで表せます。
iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}
マップも同じく3要素のタプルになります。
iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}
さらに、変数の宣言もタプルで示されます。ただし、第3要素はアトムです。
iex> quote do: x
{:x, [], Elixir}
より複雑な式も3要素タプルの組み合わせで表せます。そのとき、タプルが入れ子のツリー構造になることも少なくありません。たとえば、変数に値を代入したときです。
iex> quote do: x = 1
{:=, [], [{:x, [], Elixir}, 1]}
多くの言語ではこのような表現を抽象構文木(AST)と呼びます。Elixirの用語では内部表現(quoted expression)です。
iex> quoted = quote do: sum(1, 2 + 3, 4)
{:sum, [], [1, {:+, [context: Elixir, import: Kernel], [2, 3]}, 4]}
内部表現をもとのコードの文字列表現にしたい場合もあるでしょう。そのときには、Macro.to_string/2
をお使いください(第2引数は関数でデフォルト値がfn _ast, string -> string end
)。
iex> Macro.to_string(quoted)
"sum(1, 2 + 3, 4)"
quote/2
から返されるタプルの3要素は、つぎのような形式です。
{atom | tuple, list, list | atom}
- 第1要素: アトムまたは3要素のタプル
- 第2要素: メタデータを納めたキーワードリスト
- たとえば、数値やコンテキスト
- 第3要素: 呼び出された関数の引数リストまたはアトム
- アトムの場合タプルは変数を意味する
つぎの5つのElixirリテラルは、quote/2
の引数に渡したとき、タプルでなく値がそのまま返されます。
リテラル | 例 |
---|---|
アトム | :sum |
数値 | 1.0 |
リスト | [1, 2] |
文字列 | "string" |
2要素のタプル | {key, value} |
ほとんどのElixirのコードは、内部表現として表すことができます。たとえば、式のツリー構造が組み合わさった例です。
iex> quote do: String.upcase("foo")
{{:., [], [{:__aliases__, [alias: false], [:String]}, :upcase]}, [], ["foo"]}
if/2
マクロの構文は、do
/end
ブロックを使わずに、ひとつの式で表せます。この場合、内部表現はつぎのとおりです。
iex> quote do: if(true, do: :this, else: :that)
{:if, [context: Elixir, import: Kernel], [true, [do: :this, else: :that]]}
do
/end
ブロックを使った場合も、内部表現は変わりません。
iex> quote do
...> if true do
...> :this
...> else
...> :that
...> end
...> end
{:if, [context: Elixir, import: Kernel], [true, [do: :this, else: :that]]}
unquote/1による式の値の取り出し
quote/2
が返す内部表現は、ひとまとまりのコードを表します。たとえば、値が納められた変数を用いたとき、内部表現に示されるのは識別子です。
iex> number = 13
13
iex> Macro.to_string(quote do: 11 + number)
"11 + number"
しかし、場合によっては変数値を確かめたい場合もあるでしょう。値を取り出して式に差し込むときに使うのは、unquote/1
です。なお、このマクロはquote/2
とともに用いなければなりません。
iex> Macro.to_string(quote do: 11 + unquote(number))
"11 + 13"
関数に変数を渡して呼び出すと、内部表現の中に変数が入れ子のタプルで示されます。
iex> quote do
...> sum(1, number, 3)
...> end
{:sum, [], [1, {:number, [], Elixir}, 3]}
この場合も、変数をunquote/1
に渡せば、変数値を取り出して内部表現に差し込めるのです。
iex> quote do
...> sum(1, unquote(number), 3)
...> end
{:sum, [], [1, 13, 3]}
また、アトムをunquote/1
に渡して、名前をつけた関数が表せます。
iex> func = quote do: unquote(:hello)(:world)
{:hello, [], [:world]}
iex> Macro.to_string(func)
"hello(:world)"
リスト内に別のリストの値を加えて、内部表現にしたいこともあるでしょう。この場合、unquote/1
で差し込んだリストは、入れ子として示されます。
iex> inner = [3, 4, 5]
[3, 4, 5]
iex> Macro.to_string(quote do: [1, 2, unquote(inner), 6])
"[1, 2, [3, 4, 5], 6]"
リストから要素を取り出して内部表現に加えたいとき用いるのが、unquote_splicing/1
です。
iex> Macro.to_string(quote do: [1, 2, unquote_splicing(inner), 6])
"[1, 2, 3, 4, 5, 6]"
このマクロは、リストだけでなく関数の引数としてリスト要素を加える場合にも使えます。
iex> Macro.to_string(quote do: sum(1, 2, unquote_splicing(inner), 6))
"sum(1, 2, 3, 4, 5, 6)"
unquote/1
とunquote_splicing/1
は、マクロを扱うときにとても役立ちます。マクロを書く開発者が、ひとまとまりのコードを受け取って、そこにまた別のコードが差し込めるからです。コードを変換したり、記述して、コンパイル時にコードを生成するために用いられます。
Macro.escape/2によるエスケープ
内部表現は、3要素のタプルを基本とします。たとえば、マップはそのままでは内部表現として使えません。4要素以上のタプルも同じです。そうした値は、内部表現に変えなければならないのです。
iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}
Macro.escape/2
を用いても内部表現は得られます。
iex(23)> Macro.escape(%{1 => 2})
{:%{}, [], [{1, 2}]}
また、Macro.escape/2
を使えば、変数から取り出した値が内部表現にできるのです。
iex> map = %{hello: :world}
%{hello: :world}
iex> quote do: map
{:map, [], Elixir}
iex> quote do: unquote(map)
%{hello: :world}
iex> Macro.escape(map)
{:%{}, [], [hello: :world]}
マクロは内部表現を受け取り、内部表現を返さなければなりません。しかし、マクロの実行時に値を扱わなければならないことがあります。そのとき、値と内部表現はよく区別してください。Elixirの標準の値(リストやマップ、プロセス、参照など)と内部表現は分けて扱うことが大切なのです。
整数やアトム、および文字列は、その値を表す内部表現があります。そのほかに、たとえばマップは内部表現に変換しなければなりません。あるいは、関数や参照は値を内部表現にはできないのです。quote/2
やunquote/1
などについて詳しくは「Kernel.SpecialForms」、またMacro.escape/1
と関連する関数は「Macro」モジュールをご参照ください。
Top comments (0)