Elixirメタプログラミング 01: 内部表現 ー quote/2とunquote/1

gumitech

gumi TECH

Posted on July 2, 2019

Elixirメタプログラミング 01: 内部表現 ー quote/2とunquote/1

本稿は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]}
Enter fullscreen mode Exit fullscreen mode

タプルの第1要素は関数名、第2要素がメタデータを含むリスト、そして第3要素には引数がリストで示されます。

関数だけでなく、演算子も3要素のタプルで表せます。

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

マップも同じく3要素のタプルになります。

iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}
Enter fullscreen mode Exit fullscreen mode

さらに、変数の宣言もタプルで示されます。ただし、第3要素はアトムです。

iex> quote do: x
{:x, [], Elixir}
Enter fullscreen mode Exit fullscreen mode

より複雑な式も3要素タプルの組み合わせで表せます。そのとき、タプルが入れ子のツリー構造になることも少なくありません。たとえば、変数に値を代入したときです。

iex> quote do: x = 1
{:=, [], [{:x, [], Elixir}, 1]}
Enter fullscreen mode Exit fullscreen mode

多くの言語ではこのような表現を抽象構文木(AST)と呼びます。Elixirの用語では内部表現(quoted expression)です。

iex> quoted = quote do: sum(1, 2 + 3, 4)
{:sum, [], [1, {:+, [context: Elixir, import: Kernel], [2, 3]}, 4]}
Enter fullscreen mode Exit fullscreen mode

内部表現をもとのコードの文字列表現にしたい場合もあるでしょう。そのときには、Macro.to_string/2をお使いください(第2引数は関数でデフォルト値がfn _ast, string -> string end)。

iex> Macro.to_string(quoted)
"sum(1, 2 + 3, 4)"
Enter fullscreen mode Exit fullscreen mode

quote/2から返されるタプルの3要素は、つぎのような形式です。

{atom | tuple, list, list | atom}
Enter fullscreen mode Exit fullscreen mode
  • 第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"]}
Enter fullscreen mode Exit fullscreen mode

if/2マクロの構文は、do/endブロックを使わずに、ひとつの式で表せます。この場合、内部表現はつぎのとおりです。

iex> quote do: if(true, do: :this, else: :that)
{:if, [context: Elixir, import: Kernel], [true, [do: :this, else: :that]]}
Enter fullscreen mode Exit fullscreen mode

do/endブロックを使った場合も、内部表現は変わりません。

iex> quote do
...>   if true do
...>     :this
...>   else
...>     :that
...>   end
...> end
{:if, [context: Elixir, import: Kernel], [true, [do: :this, else: :that]]}
Enter fullscreen mode Exit fullscreen mode

unquote/1による式の値の取り出し

quote/2が返す内部表現は、ひとまとまりのコードを表します。たとえば、値が納められた変数を用いたとき、内部表現に示されるのは識別子です。

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

しかし、場合によっては変数値を確かめたい場合もあるでしょう。値を取り出して式に差し込むときに使うのは、unquote/1です。なお、このマクロはquote/2とともに用いなければなりません。

iex> Macro.to_string(quote do: 11 + unquote(number))
"11 + 13"
Enter fullscreen mode Exit fullscreen mode

関数に変数を渡して呼び出すと、内部表現の中に変数が入れ子のタプルで示されます。

iex> quote do
...>   sum(1, number, 3)
...> end
{:sum, [], [1, {:number, [], Elixir}, 3]}
Enter fullscreen mode Exit fullscreen mode

この場合も、変数をunquote/1に渡せば、変数値を取り出して内部表現に差し込めるのです。

iex> quote do
...>   sum(1, unquote(number), 3)
...> end
{:sum, [], [1, 13, 3]}
Enter fullscreen mode Exit fullscreen mode

また、アトムをunquote/1に渡して、名前をつけた関数が表せます。

iex> func = quote do: unquote(:hello)(:world)
{:hello, [], [:world]}
iex> Macro.to_string(func)
"hello(:world)"
Enter fullscreen mode Exit fullscreen mode

リスト内に別のリストの値を加えて、内部表現にしたいこともあるでしょう。この場合、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]"
Enter fullscreen mode Exit fullscreen mode

リストから要素を取り出して内部表現に加えたいとき用いるのが、unquote_splicing/1です。

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

このマクロは、リストだけでなく関数の引数としてリスト要素を加える場合にも使えます。

iex> Macro.to_string(quote do: sum(1, 2, unquote_splicing(inner), 6))
"sum(1, 2, 3, 4, 5, 6)"
Enter fullscreen mode Exit fullscreen mode

unquote/1unquote_splicing/1は、マクロを扱うときにとても役立ちます。マクロを書く開発者が、ひとまとまりのコードを受け取って、そこにまた別のコードが差し込めるからです。コードを変換したり、記述して、コンパイル時にコードを生成するために用いられます。

Macro.escape/2によるエスケープ

内部表現は、3要素のタプルを基本とします。たとえば、マップはそのままでは内部表現として使えません。4要素以上のタプルも同じです。そうした値は、内部表現に変えなければならないのです。

iex> quote do: %{1 => 2}
{:%{}, [], [{1, 2}]}
Enter fullscreen mode Exit fullscreen mode

Macro.escape/2を用いても内部表現は得られます。

iex(23)> Macro.escape(%{1 => 2})
{:%{}, [], [{1, 2}]}
Enter fullscreen mode Exit fullscreen mode

また、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]}
Enter fullscreen mode Exit fullscreen mode

マクロは内部表現を受け取り、内部表現を返さなければなりません。しかし、マクロの実行時に値を扱わなければならないことがあります。そのとき、値と内部表現はよく区別してください。Elixirの標準の値(リストやマップ、プロセス、参照など)と内部表現は分けて扱うことが大切なのです。

整数やアトム、および文字列は、その値を表す内部表現があります。そのほかに、たとえばマップは内部表現に変換しなければなりません。あるいは、関数や参照は値を内部表現にはできないのです。quote/2unquote/1などについて詳しくは「Kernel.SpecialForms」、またMacro.escape/1と関連する関数は「Macro」モジュールをご参照ください。

💖 💪 🙅 🚩
gumitech
gumi TECH

Posted on July 2, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related