Erlang の Meta Programming 機能を smerl で簡単に実現する

個人的な意見であり、決して一般的な意見ではないと思うのだけれど、Erlang 言語でプログラムをしていると、高階関数やメタ機能の利用が面倒で困る。
つい最近も、ちょっとしたモジュールを書いている際、モジュールへ動的に関数を追加できれば、コード行数が減り、モジュールの使い方も簡単になるケースがあったで、erl_scancompile のドキュメントを眺めていたのだが・・・smerl(Simple Metaprogramming for Erlang) というモジュールを見つけたのでメモを残す。

取得方法

smerl は、ErlyWeb に組み込まれているモジュールである為、ErlyWeb のリポジトリからソースを取得する事になる。

$ svn co http://erlyweb.googlecode.com/svn/trunk/src/smerl smerl

簡単な例

$ erl
1> {ok, C1} = smerl:for_module(mnesia).
2> {ok, C2} = smerl:add_func(C1, "foo() -> 1 + 1.").
3> smerl:compile(C2).
4> mnesia:foo().

詳細

モジュールのメタデータを作る

smerl は、モジュールのメタデータ(meta_mod() 型)を操作し、それをコンパイルする事でメタプログラミングを実現する。という事で、メタデータの作り方は、下記の通り。

新規に作成 C = smerl:new(foo).
ロード済みモジュールから作成 {ok, C} = smerl:for_module(mnesia).
ファイルから作成 {ok, C} = smerl:for_file("/opt/local/lib/erlang/lib/mnesia-4.4.2/src/mnesia.erl").

Include ディレクトリを指定できる smerl:for_module/2 や smerl:for_file/2 も存在する。
使い方の例。

$ cd /path/to/kai/src
$ erl
1> C = smerl:for_file("./kai_config.erl", ["../include"]).
2> smerl:compile(C).

kai_config.erl は、内部で「-include("kai.hrl").」としているので、kai.hrl が配置されいてるディレクトリを指定す必要がある。

meta_mod() 型宣言は、多分、下記ような感じ。(token() については、erl_parse を参照の事)

%% @type line() = integer().

%% @type tag() = atom().
%% @type token() = {tag(), line()} | {tag(), line(), term()}.

%% @type param() = token().
%% @type guard() = token().
%% @type expr() = token().
%% @type clause() = {clause, line(), [param()], [guard()], [expr()]}

%% @type func_name() = atom().
%% @type arity() = integer().
%% @type func_form() = {function, line(), func_name(), arity(), [clause()]}

%% @type attribute_name() = atom().
%% @type attribute_value() = term().
%% @type attribute() = {attribute, line(), attribute_name(), attribute_value()}

%% @type module() = atom().
%% @type file() = string().
%% @type exports() = [{func_name(), arity()}].
%% @type forms() = [func_form() | attribute()].
%% @type export_all() = true | false.

%% @type meta_mod() = {meta_mod, module(), file(), exports(), forms(), export_all()}.
メタデータコンパイルする

smerl を利用したメタプログラミングの手順は、主に下記の通り。

  1. smerl:new/1 等で、メタデータを取得する
  2. メタデータを操作する
  3. smerl:compile/2 等でメタデータコンパイルする

smerl:compile/2 は、第一引数にメタデータを、第ニ引数にコンパイルオプションを指定する。
smerl:compile/2 は、compile:forms/2 を利用しており、第二引数コンパイルオプションはそのまま、compile:forms/2 の第二引数となる。
compile:forms/2 の第二引数は、こちらの compile:file/2 の辺りを参照の事。

コンパイルオプションを省略し、smerl:compile/1 を評価した場合は・・・

smerl:compile(C).

下記と同等となる。

smerl:compile(C, [report_errors, report_warnings, return_errors]).
メタデータソースコードを確認する

smerl:to_src/1 の第一引数に meta_mod() を指定すると、ソースコードを string() 型で取得できる。

$ erl
1> {ok, C} = smerl:for_module(smerl).
2> SmerlSourceCode = smerl:to_src(C).

ソースコードをファイルに保存する場合は smerl:to_src/2 を使う。

3> smerl:to_src(C, "./smerl.erl").
メタデータを操作する(関数以外)
対象 取得 設定 削除
module() Module = smerl:get_module(C). NewC = smerl:set_module(C, NewModel).
exports() Exports = smerl:get_exports(C). NewC = set_exports(C, NewExports). NewC = remove_export(C, FuncName, Arity).
forms() Forms = smerl:get_forms(C). NewC = set_forms(C, NewForms).
export_all() NewExportAll = smerl:get_export_all(C). NewC = smerl:set_export_all(C, NewExportAll).
attribute_value() {ok, Attr} = smerl:get_attribute(C, AttrName).

モジュール名を変える例。(mnesia を foo にする)

$ erl
1> {ok, C1} = smerl:for_module(mnesia).
2> C2 = smerl:set_module(C1, foo).
3> smerl:compile(C2).
4> foo:start().

export する関数を変更する例。(smerl の export 関すは new/0 のみとする)

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> C2 = smerl:set_exports(C1, [{new, 0}]).
3> smerl:compile(C2).
4> {ok, C1} = smerl:for_module(smerl).
** exception error: undefined function smerl:for_module/1

全ての関数を export する例。(smerl に「-export(all).」を追加する)

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> C2 = smerl:set_export_all(C, true).
3> smerl:compile(C2).
4> smerl:module_info(exports).

attribute を取得する例。(smerl の author を取得する)

$ erl
1> {ok, C} = smerl:for_module(smerl).
2> {ok, Author} = smerl:get_attribute(C, author).
{ok,"Yariv Sadan (yarivsblog@gmail.com, http://yarivsblog.com"}

attribute の一覧を取得する例。(get_forms/1 を利用する)

$ erl
1> {ok, C} = smerl:for_module(smerl).
2> Forms = smerl:get_forms(C).
3> Attrs = lists:filter(fun ({attribute, _, _, _}) -> true; (_) -> false end, Forms).
メタデータを操作する(関数関連)

関数が存在するか確認する。

$ erl
1> {ok, C} = smerl:for_module(smerl).
2> smerl:has_func(C, compile, 1).
true
3> smerl:has_func(C, foo, 1).
false

関数のメタデータ func_form() を取得する。

$ erl
1> {ok, C} = smerl:for_module(smerl).
2> {ok, F} = smerl:get_func(C, compile, 1).

関数を追加する。

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> {ok, C2} = smerl:add_func(C1, "foo(X) -> X + 1.").
3> smerl:compile(C2).
4> smerl:foo(1).

更に、追加した関数 foo/1 を他のモジュールへコピーする。

5> {ok, FooFunc} = smerl:get_func(C2, foo, 1).
6> {ok, C3} = smerl:for_module(mnesia).
7> {ok, C4} = smerl:add_func(C3, FooFunc).
8> smerl:compile(C4).
9> mnesia:foo(1).

上記の二つの例の通り、add_func/2 の第二引数は、文字列でも func_form() でも良い。

add_func/2 は、exports() に関数を追加してしまう。追加した関数を export したくない場合は、add_func/3 を使用する。

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> {ok, C2} = smerl:add_func(C1, "foo(X) -> X + 1.", false).
3> {ok, C3} = smerl:add_func(C2, "foo() -> foo(1).").
4> smerl:compile(C3).
5> smerl:foo().
2
5> smerl:foo(1).
** exception error: undefined function smerl:foo/1

関数を削除する。

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> C2 = smerl:remove_func(C1, compile, 1).
3> smerl:compile(C2).
ok
4> smerl:compile(C2).
** exception error: undefined function smerl:compile/1

関数を入れ替える。

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> {ok, C2} = smerl:replace_func(C1, "compile(_MetaMod) -> rejection.").
3> smerl:compile(C2).
ok
4> smerl:compile(C2).
rejection

replace_func/2 の第二引数は、func_form() でも良い。
replace_func/2 は、内部で remove_func/3 と add_func/2 を使っているだけ。よって、入れ替えた関数は export されてしまう。(replace_func/3 は存在しない)

Mix-in する。

$ cat parent.erl
-module(parent).
-export([foo/2, bar/2]).

foo(X, Y) -> io:fwrite("parent:foo"), X + Y.
bar(X, Y) -> io:fwrite("parent:bar"), X - Y.
$ cat child.erl
-module(child).
-export([bar/2, baz/2]).

bar(A, B) -> io:fwrite("child:bar"), A * B.
baz(X, Y) -> io:fwrite("child:baz"), X / Y.
$ erl
1> C1 = smerl:extend(parent, child).
2> smerl:to_src(C1, "./new_child1.erl").
$ cat new_child1.erl
-module(child).
-export([foo/2, bar/2, baz/2]).

foo(X, Y) -> parent:foo(X, Y).
baz(X, Y) -> io:fwrite("child:baz"), X / Y.
bar(A, B) -> io:fwrite("child:bar"), A * B.

child:foo/2 から、parent:foo/2 を呼んでいるが、下記のようにすると parent:foo/2 そのものが child モジュールに追加される。

3> C2 = smerl:extend(parent, child, 0, [copy]).
4> smerl:to_src(C2, "./new_child2.erl").
$ cat new_child2.erl
-module(child).
-export([foo/2, bar/2, baz/2]).

foo(X, Y) -> io:fwrite("parent:foo"), X + Y.
baz(X, Y) -> io:fwrite("child:baz"), X / Y.
bar(A, B) -> io:fwrite("child:bar"), A * B.

今の所、copy 以外のオプションは存在しない。
copy を指定しても、parent の export に含まれない関数は、Mix-in の対象とならない。

smerl:extend/3 や smerl:extend/4 の第三引数 ArityDiff は、数値を指定する。extend 内の処理は下記の通り。

  1. child の export で指定されている全ての関数のアリティに ArityDiff を加える
  2. parent の export で指定されている全ての関数から「1.」を取り除いて、Mix-in 候補とする

ドキュメントには、embed_all() 等の後に extend() を行なう際に役立つみたいな事が書かれている。

$ erl
1> {ok, C1} = smerl:for_module(child).
2> C2 = smerl:embed_all(C1, [{'A', 5}]).
3> C3 = smerl:extend(parent, C2, 1).
4> smerl:to_src(C3, "./new_child3.erl").
$ cat new_child3.erl
-module(child).
-export([foo/2, bar/1, baz/2]).

foo(X, Y) -> parent:foo(X, Y).
baz(X, Y) -> io:fwrite("child:baz"), X / Y.
bar(B) -> io:fwrite("child:bar"), 5 * B.

child:bar/1 を child:bar/2 として扱ので、parent:bar/2 を Mix-in の対象から外す事ができる。

関数のメタデータを操作する

名前を変更する。

$ erl
1> {ok, C} = smerl:for_module(smerl).
2> {ok, F1} = smerl:get_func(C, compile, 1).
3> F2 = smerl:rename(F1, foo).

カリー化。

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> {ok, F1} = smerl:get_func(C1, for_module, 1).
3> {ok, F2} = smerl:curry(F1, smerl).
4> {ok, C2} = smerl:add_func(C1, F2).
5> smerl:compile(C2).
6> {ok, C1} = smerl:for_module().

smerl:curry/2 の第ニ引数は、束縛する値のリストを渡す。(上記例のように、束縛したい引数が一つの場合は、リスト以外でも良い)

上記例の 1 〜 3 は、下記例と同等。

{ok, F} = smerl:curry(smerl, for_module, 1, smerl).

名前を変えたい場合は、下記のようにできる。

{ok, F} = smerl:curry(smerl, for_module, 1, smerl, for_smerl_module).

更に、2 〜 4 は、下記と同等。

{ok, C2} = smerl:curry_add(C1, for_module, 1, smerl).

名前を変えたい場合は、下記のようにできる。

{ok, C2} = smerl:curry_add(C1, for_module, 1, smerl, for_smerl_module).

元となった関数を削除しても良い場合は、下記のようにできる。

{ok, C2} = smerl:curry_replace(C1, for_module, 1, smerl).

smerl:curry_add/4 と smerl:curry_replace/4 には、対になる smerl:curry_add/3 と smerl:curry_replace/3 があり、第二引数と第三引数を func_form() に置き換える事ができる。

任意の引数を束縛する。

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> {ok, F1} = smerl:get_func(C1, to_src, 2).
3> F2 = smerl:embed_params(F1, [{'FileName', "./test.erl"}]).
4> F3 = smerl:rename(F2, dump_to_file).
5> {ok, C2} = smerl:add_func(C1, F3).
6> smerl:compile(C2).
7> smerl:dump_to_file(C2).

smerl:embed_params/2 の第ニ引数は、変数名の atom() をキーにした proplist を指定する。

上記例の 2 〜 5 は、下記例と同等。

{ok, C2} = smerl:embed_params(C1, to_src, 2, [{'FileName', "./test.erl"}], dump_to_file).

smerl:embed_params/5 の第五引数を省略し、smerl:embed_params/4 を利用すると、名前を変更せずに追加する。

カリー化の失敗例その1。

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> {ok, C2} = smerl:curry_add(C1, new, 1, foo).
3> smerl:compile(C2).
/path/to/smerl.erl:131: variable 'ModuleName' is unbound
{error,[{"/path/to/smerl.erl",
         [{131,erl_lint,{unbound_var,'ModuleName'}}]}],
       []}

これは、下記のような Source になるので、エラーとなって当たり前。

new() when is_atom(ModuleName) ->
    #meta_mod{module = foo}.

残念ながら、smerl のカリー化は、ガード条件を修正してくれない。

カリー化の失敗例その2。

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> {ok, C2} = smerl:curry_replace(C1, for_module, 1, smerl).
3> smerl:compile(C2).
/path/to/smerl.erl:686: function for_module/1 undefined
/path/to/smerl.erl:999: function for_module/1 undefined
{error,[{"/path/to/smerl.erl",
         [{686,erl_lint,{undefined_function,{for_module,1}}},
          {999,erl_lint,{undefined_function,{for_module,1}}}]}],
       []}

smerl:curry_replace/4 を使うと、元となった関数を削除してくれるのだが、元となった関数を呼び出している箇所を置き換えてくれるわけではないので、エラーとなる。

カリー化の失敗例その3。

$ erl
1> {ok, C1} = smerl:for_module(smerl).
2> C2 = smerl:embed_all(C1, [{'ModuleName', foo}]).
3> smerl:compile(C2).
/path/to/smerl.erl:131: variable 'ModuleName' is unbound
/path/to/smerl.erl:137: function for_module/2 undefined
/path/to/smerl.erl:151: variable 'ModuleName' is unbound
/path/to/smerl.erl:153: variable 'ModuleName' is unbound
{error,[{"/path/to/smerl.erl",
         [{131,erl_lint,{unbound_var,'ModuleName'}},
          {137,erl_lint,{undefined_function,{for_module,2}}},
          {151,erl_lint,{unbound_var,'ModuleName'}},
          {153,erl_lint,{unbound_var,'ModuleName'}}]}],
       []}

smerl:embed_all/2 を使うと、モジュール内の全ての関数の任意の引数を一括で束縛できるのだが・・・失敗例その1やその2と同様の理由でエラーとなる事がある。

Erlangクロージャをサポートするので、個人的に、メタプログラミングによるカリー化を行なう必要性を感じない。

使ってみた感想

meta_mod() 型は、ただの Erlang 型なので、smerl に存在しない操作が必要になったとしても、かなり自由にメタプログラミング可能。
また、erlang の OO と組み合わせても正しく動作したので、かなり実用的な印象を受けた。
ただ、curry とか embed は、ソースを追って動作を正しく理解してないとハマる。カリー化は、標準的な機能だけで実現できるので、あえて smerl を使う必要性を感じない。(速度は、こちらの方が早い)
当たり前の話だが、やりすぎると、コードの可読性が著しく低下するので、利用時に、本当にメタ操作が必要か否かよく考える必要がある。