無名関数での再帰処理
elixirでの無名関数はfnスペシャルフォームで定義できるが、無名関数のため、自分自身を呼び出すような処理は記述できない。
たとえば、フィボナッチ数を求める関数は、
fn (0) -> 0 (1) -> 1 (n) -> self(n-1) + self(n - 2)
のように書くことは出来ない。
Lispでは、そういうときのためにlabelsというスペシャルフォームがあり、
(setf x (labels ((self (n) (cond ((eq n 0) 0) ((eq n 1) 1) (t (+ (self (- n 1)) (self (- n 2))))))) )) (funcall x 10) 55
と書ける。elixirでも同じように書きたいが、selfはpidを返す関数なのでmeという疑似変数を導入して
> a = fn (0) -> 0 (1) -> 1 (n) -> me(n-1) + me(n-2) end >a.(10) > 55
こんなふうに書けたら便利な気がする。
無名関数から自分自身を呼び出す
とりあえず、無名関数を変数に保存しておいて使ってみると、
a = fn(0) -> 0 (1) -> 1 (n) -> a.(n-1) + a.(n-2) end
ではfnの評価時にaが定義されていないので駄目。なので、仮引数としてaを
渡してやれば良い。
b = fn(a) -> fn(0) -> 0 (1) -> 1 (n) -> a.(n-1) + a.(n-2) end end
これを評価すると、無名関数を返す無名関数関数が出来上がる。
b.(b)を評価すると、
> c = b.(b) #Function<6.80484245 in :erl_eval.expr/5> > c.(3) ** (ArithmeticError) bad argument in arithmetic expression :erlang.+(#Function<6.80484245 in :erl_eval.expr/5>, #Function<6.80484245 in :erl_eval.expr/5>)
おお、エラーだ。よく見ると、上のfnではaはbつまり、関数を引数にとることになっているが、上記コードではn-1つまり2を渡している。実際、b.(b)がcつまり、求める無名関数の筈なので、fn中のaをa.(a)に書き換えてみる。
b = fn(a) -> fn(0) -> 0 (1) -> 1 (n) -> a.(a).(n-1) + a.(a).(n-2) end end
これを同様にして評価していくと、
> c = b.(b) #Function<6.80484245 in :erl_eval.expr/5> > c.(3) 2
うまくいった。つまり、アウトラインはこんな感じ。
defmacro afn(function) do cfunc = translate(:me, function) ## function中のmeをme.(me).に変更 quote do me = fn(me) -> unquote(function) end me.(me) end end
マクロとしてまとめる
translate/2はelixirのASTをトラバースしながらパターンを置き換えることになる。
ASTは以下のリテラルから成り立つ。
def ast_traverse(a, _f) when is_atom(a) or is_number(a) or is_binary(a) do a end def ast_traverse({a, b}, _f) do {a, b} end def ast_traverse(a, _f) when is_list(a) do a end
次に、関数呼び出しを含むスペシャルフォームは以下の種類がある。
{a, b, c} when :"->" == a and is_list(c) -> 節 {a, b, c} when is_atom(a) and is_list(c) -> 関数実行 {a, b, c} when is_tuple(a) and size(a) == 3 and is_list(c) -> 関数戻り値を関数として実行 {a, b, c} when is_atom(a) and is_atom(c) -> 変数
それぞれに対応してパターンを記述すると、
def ast_traverse({a, b, c}, f) do {a, b, c} = f.({a, b, c}) case {a, b, c} do {a, b, c} when :"->" == a and is_list(c) -> {a, b, Enum.map(c, fn(x) -> {args, meta, node} = x {Enum.map(args, &(ast_traverse(&1, f))), meta, ast_traverse(node, f)} end)} {a, b, c} when is_atom(a) and is_list(c) -> {ast_traverse(a, f), b, Enum.map(c, &(ast_traverse(&1, f)))} {a, b, c} when is_tuple(a) and size(a) == 3 and is_list(c) -> {ast_traverse(a, f), b, Enum.map(c, &(ast_traverse(&1, f)))} {a, b, c} when is_atom(a) and is_atom(c) -> {ast_traverse(a, f), b, c} end end
これを使ってtranslate/3は以下のように書ける。
def translate(tree, a, module // nil) do ast_traverse(tree, fn({^a, b, c}) when is_list(c) -> {{:., b, [{{:., b, [{a, b, module}]}, b, [{a, b, module}]}]}, b, c} (x) -> x end) end
これでlispのlabelsもどきを実施するdo_label/2とafn/1をやっと定義できるようになった。
def do_label({n, line, module} = nvar, clouse) do {n, line, module} = nvar c = translate(clouse, n, module) c2 = {:fn, line, [{:"->", line, [{[nvar], line, c}]}]} r = quote do me = unquote(c2) me.(me) end r end defmacro afn(clouse) do r = do_label({:me, [], nil}, clouse) r end
これをモジュールMMとかに入れておいて、コンパイルして使ってみる。
iex(106)> c("mm.ex") [MMM, MM] iex(107)> require MM; iex(108)> r = MM.afn(fn (0) -> 0 ...(108)> (1) -> 1 ...(108)> (n) -> me(n-1) + me(n-2) end) #Function<6.80484245 in :erl_eval.expr/5> iex(109)> r.(10) 55 iex(110)>
無名関数の内部でmeという関数名で自分自身を参照できるようになった。