elixirはプログラマの万能薬になるか その3

前回はrubyなところを主に説明してきたので、いよいよ今回はLispな所であり、個人的に最もエキサイティングだと感じているメタプログラミングについて記述する。

メタプログラミング

プログラムを書くプログラムを書く事をメタプログラミングと呼ぶ。Cのプリプロセッサや、yacc等のコード生成系もメタプログラムの範囲に含める場合があるようだが、Lispが最も有名であり、徹底されている。LispはプログラムがLispのS式で表現されているため、Lispの全能力使ってメタプログラミングが行える。それ故に、Lispは他の言語とは次元の違う強力さを持っている。プログラム言語がデータ構造として表現できる事(homoiconic)と、構文解析とコード生成の間にマクロの層があることがこの強力さの源になっている。

一方、elixirはというと、ruby風味のシンタックスであるにも関わらず、Lisp並のメタプログラミング機能を手に入れている。
では、どうやっているのか。これから見てみよう。カギはquoteとdefmacroだ。

elixirにおける構文のhomoiconic表現

elixirはhomoiconic言語、つまり、任意のelixirプログラムはelixirのデータ構造を使用して表現できる。リストの長さを返すlength/1で試してみよう。quote do:を使う。

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

この3要素のタプルがlength/1に対応する表現だ。一般的には

{ Tuple | Atom, Integer, List | Atom }
第一要素
アトム(関数名や変数名)か、他のタプル表現
第二要素
行番号
第三要素
関数への引数となるリストか、変数の属性を表すアトム(nilあるいはquoted)

elixirは後で記述する5種類のリテラルをのぞくと、すべては関数である。タプル自身、演算子や代入、do -- endブロックでさえもだ。さあ let's quote do!

iex> quote do: { 1,2,3 }
{:"{}",0,[1,2,3]}
iex> quote do: 1 + 2    
{:"+",0,[1,2]}
iex> x=1
1
iex> quote do: x
{:x,0,:quoted}
iex> quote do: x = 2
{:"=",0,[{:x,0,:quoted},2]}
iex> quote do
...> 1+2
...> 2*3
...> end
{:__block__,0,[{:"+",0,[1,2]},{:"*",0,[2,3]}]}
iex> 

最後のブロックの例のように、タプル表現はいくらでも入れ子になってelixirの式を表現する。
そしてquoteしてもそれ自身を返すという意味でリテラルとなるのは、以下のとおり。

iex> quote do: :sum # アトム
:sum
iex> quote do: 1   # 数値
1
iex> quote do: 2.0 # 数値
2.00000000000000000000e+00
iex> quote do: [1,2] # リスト
[1,2]
iex> quote do: "bin" # バイナリ
"bin"

これでマクロを定義する準備ができた。

defmacroとunquote

マクロはモジュール中でdefmacroで作成する。ありきたりだが、ifとよく似たunlessを作ってみよう。マクロは自身が定義されたモジュールの外からしか利用できないことに注意して、

iex> defmodule M do
...> defmacro unless(clause, options) do
...>  quote do: if !unquote(clause), unquote(options)
...> end
...> end
{:unless,2}
iex> x = 1               
1
iex> require M; M.unless x > 0, do: true
nil
iex> require M; M.unless x > 3, do: true
true
iex> 

なんとなくそれっぽく動いているようだ。unquoteは、引数をquoteされた式中にそのままのコンテキストで埋め込むマクロである。unquoteがないとコンテキストが分断されてしまう。

iex> x = 1               
1
iex> quote do: x + 1 + 1
{:"+",0,[{:"+",0,[{:x,0,:quoted},1]},1]}
iex> quote do: unquote(x + 1) + 1
{:"+",0,[2,1]}
iex> 

これだけだと、わかりにくいかもしれないので、unlessでunquoteしないで定義してみよう。

iex> defmodule M2 do
...> defmacro unless(clause, options) do
...>  quote do: if !clause, options
...> end
...> end
nofile:2: variable clause is unused
nofile:2: variable options is unused
{:unless,2}
iex> require M2; M2.unless x > 0, do: true
** (::FunctionClauseError) no function clause matching: ::Elixir::Builtin.if({:"!",1,[{:clause,1,:quoted}]}, {:options,1,:quoted})
    lib/elixir/builtin.ex:830: ::Elixir::Builtin.if({:"!",1,[{:clause,1,:quoted}]}, {:options,1,:quoted})
    nofile:1: ::Elixir::Builtin.if/2
    src/elixir_dispatch.erl:104: :elixir_dispatch.dispatch_macro/6
    src/elixir_dispatch.erl:111: :elixir_dispatch.dispatch_macro/6
    lists.erl:1278: :lists.mapfoldl/3
    lists.erl:1279: :lists.mapfoldl/3
    src/elixir.erl:66: :elixir.eval_forms/3
    src/elixir.erl:49: :elixir.eval/5
iex> 
Elixir::Builtin.if({:"!",1,[{:clause,1,:quoted}]}, {:options,1,:quoted})

となっているとおり、変数clauseとoptionsとしてそのままquoteされている。そうではなく、unlessに渡したclauseとoptionsの中身をそのまま置き換えてほしかった筈だ。それをしてくれるのがunquoteになる。
言い換えると、unquoteはquoteされたツリーに式を組み込むメカニズムで、メタプログラミングの本質だ。

マクロの再帰

実用的ではないが、フィボナッチ数を計算するマクロを実装してみよう。

defmodule Self do
 defmacro fibm(0) do
  quote do: 0
 end
 defmacro fibm(1) do
  quote do: 1
 end
 defmacro fibm(x) do
  quote do: unquote(fibm(x-1)) + unquote(fibm(x-2))
 end
 def fibmm(x) do
   fibm(x)
 end

Self.fibmm/1関数はfibmマクロを呼び出し、置き換え結果を返す。モジュールの内部の関数からマクロを呼び出すと、展開結果を評価しないのだ。評価されてしまうと、マクロの再帰ができなくなってしまうため、このようになっている。これを使ってマクロがどうquoteされていくかを観察できる。
上記のコードをiexに読み込ませて

iex> Self.fibmm(4) 
{:"+",0,[{:"+",0,[{:"+",0,[1,0]},1]},{:"+",0,[1,0]}]}
iex> require Self; Self.fibm(4)
3
iex> 

まとめ

defmacroでquoteした式にunquoteして引数を組み込むという仕組みはシンプルだが強力だ。
Doug HoyteはLET OVER LAMBDA*1で「他のすべての言語が単にlispに皮をかぶせたにすぎない」と指摘している。elixirは他のBlub言語*2と異なり、その皮をいつでもquoteで剥がし、Lispと同様にそのナマの構造をelixirで操作することができることが示せたと思う。これこそがプログラマにとっての万能薬の秘密だ。
マクロの世界へようこそ。
次回は、マクロをさらに掘り下げていきたい。