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>