elixirはプログラマの万能薬になるか その4
今回はマクロプログラミングをelixir自身を例題にしていこうと思うが、マクロをプリプロセッサと勘違いしている人も多いので、まずは入門から。
マクロ入門
Elixirのマクロは、コンパイルプロセス中において、構文解析後のツリーを入力として、別のツリーを返すフックとして機能する。そして、そのフックはElixir自身を用いて記述することができる。この記述のしやすさと、ElixirのクロージャサポートによりLispなみの拡張性を持っている*1。
フェーズ | 処理内容 | |
---|---|---|
(プリプロセッサ) | (文字列-->文字列) | |
1 | 字句解析 | 文字列-->トークン列 |
2 | 構文解析 | トークン列-->タプルによる構文ツリー |
3 | マクロ | 構文ツリー-->構文ツリー |
4 | コード生成 | 構文ツリー-->beamバイナリ生成 |
ということで、詳細は前回のエントリを参照していただきたい。
マクロプログラミングの実際
elixirではデリゲーションを行うためにdefdelegateが提供されている。それとよく似たマクロ delegate [{name, arity}|t], do: target を考えてみよう。名前以外はdefdelegateと同じ機能を持ち、あるモジュールの関数群を他のモジュールに委譲したい場合に使う事を目的としている。
Erlang標準のリストモジュールを元にMyListを作ろうとしているとしよう。reverseやmemberはもとと同じにしたいので、
defmodule MyList do delegate [reverse: 1, member: 2], to: Erlang.lists end
のように書いておきたい。そしてこれをこう展開したい。
defmodule MyList do def reverse(arg1) do apply Erlang.lists, reverse, [arg1] end def member(arg1, arg2) do apply Erlang.lists, member, [arg1, arg2] end end
ではこれを実装するマクロdelegateを考えていこう。
MyMacroモジュールに実装するとして、コアはこんな感じとなるだろう。
defmodule MyMacro defmacro delegate [{fname, arity}|t], to: func args = makeargs(arity) quote do def unquote(fname).(unquote_splicing(args)) do apply unquote(func), unquote(fname), [unquote_splicing(args)] end end end end
makeargs/1は指定された数だけの仮引数として使用できるアトムリストを返す関数であり、例えばこんな感じで実装できる。
def makeargs_1(arity) do Erlang.lists.map(fn(x) -> list_to_atom(List.flatten(Erlang.io_lib.format("arg~p", [x]))) end, Erlang.lists.seq(1,arity)) end
このmakeargs_1を実行してみると、
iex> MyMacro.makeargs_1(2) [:"arg1",:"arg2"] iex>
いい感じで動いていそうだ。これをquoteされたリストにどう変換するか考える。まず、通常の関数の引数がどのようにquoteされているかを調べてみる。
iex> quote do ...> defmodule M do ...> def func(arg1, arg2, arg3) do ...> true ...> end ...> end ...> end {:defmodule,0,[{:__ref__,0,[:M]},[{:do,{:def,0,[{:func,0,[{:"arg1",0,:quoted},{:"arg2",0,:quoted},{:"arg3",0,:quoted}]},[{:do,true}]]}}]]} iex>
つまり、{引数アトム, 0, :quoted} というタプルのリストになっている。このことから、makeargs/1は、makeargs_1/1を使い、
def makeargs(arity) do arglist = makeargs_1(arity) Erlang.lists.map(fn(x) -> {x, 0, :quoted} end, arglist) end
と書けそうだ。makeargs_1自体もmapを使っているため、fnを置き換えて、
def makeargs(arity) do Erlang.lists.map(fn(x) -> arg = list_to_atom(List.flatten(Erlang.io_lib.format("arg~p", [x]))) {arg, 0, :quoted} end, Erlang.lists.seq(1,arity))
となる。makeargs/1を実行してみると、うまく動いていることが分かる。
iex> MyMacro.makeargs(3) [{:"arg1",0,:quoted},{:"arg2",0,:quoted},{:"arg3",0,:quoted}] iex>
次にdelegate本体だが、上のコアだけの定義だと最初のfnameタプルのみが展開される形になるため、残りを再帰で実装する感じで書いてみる。
defmacro delegate [{fname, arity}|t], to: func do args = makeargs(arity) e = quote do def unquote(fname).(unquote_splicing(args)) do apply unquote(func), unquote(fname), [unquote_splicing(args)] end end case t do match: [] [e] else: [e | delegate(t, to: func)] end end
最初の一要素分をquoteしたあとは、残りを再帰的に呼び出して変換していく。unquote_splicing/1は、リストの中身だけをその場に展開することをのぞけばunquote/1と同じである事に注意すると、素直な実装だが、よく見てみると、{fname,arity}リストを一対一で quote do: def fname リストに変換する形がハッキリと見えてきくる。Erlang.lists.mapで作り直してみる。
defmacro delegate2(tuplelist, to: func) when is_list(tuplelist) do Erlang.lists.map(fn({fname, arity}) -> args = makeargs(arity) quote do def unquote(fname).(unquote_splicing(args)) do apply unquote(func), unquote(fname), unquote(args) end end end, tuplelist) end
elixir標準のEnum.map/2を使うともっと簡単に書けて、
defmacro delegate3(tuplelist, to: func) when is_list(tuplelist) do Enum.map tuplelist, fn({fname, arity}) -> args = makeargs(arity) quote do def unquote(fname).(unquote_splicing(args)) do apply unquote(func), unquote(fname), unquote(args) end end end end
となる。
このようにElixir自身がElixir自身で拡張していけるようになっている。この機能を使って、ExUnit(ユニットテストフレームワーク)やレコードやプロトコル機能、果てはif文すらもマクロとして実装されている。
Paul GrahamはOn LispでLispは拡張可能なプログラミング言語であると書いているが、Elixirもその能力を持っている事がわかっていただけるだろう。
次回は、レコード機能やプロトコル機能について説明予定。
- 作者: ポールグレアム,野田開,Paul Graham
- 出版社/メーカー: オーム社
- 発売日: 2007/03/01
- メディア: 単行本
- 購入: 10人 クリック: 146回
- この商品を含むブログ (128件) を見る