GoFのデザインパターンのなかにCommandパターンってのがあります。ここでいう「コマンド」とは、なんらかのアクション*1を閉じ込めたオブジェクトのことです。普通は、execute()とかいうメソッドが備わっていて、コマンドオブジェクトのexecute()を呼ぶと当該アクションが実行されるわけです。
Commandパターンの役者はコマンドオブジェクトだけではなくて、コマンドの順次実行を引き受けるCommand Invokerというヤツも登場します。このインボーカーには、スレッドを割り当て、キューにポストされたコマンドをひたすら実行するループを回してもらうことが多いでしょう。*2
さて、CommandパターンをErlangで書いてみると:
-module(command).
-export([start/0]).start() ->
spawn(fun main_loop/0).main_loop() ->
receive
stop ->
ok; % 終了
{command, Fun, Args} when is_function(Fun) ->
apply(Fun, Args), % コマンド実行
main_loop();
_Other -> % 無視
main_loop()
end.
えっ、これだけなの? まー、本質的部分はこんなもんじゃないの。コマンドオブジェクトにあたるデータは関数と引数をまとめたタプル。先頭にcommandという印(タグ)を付けてます。確かに、アクションを閉じ込めたモノになっているでしょ。
インボーカーはプロセスで、メッセージとしてポストされたコマンド・タプルをひたすら実行するわけね。
1> c(command).
{ok,command}
2> P = command:start().<0.122.0>
3> P!{command, fun()->io:format("Hello.~n") end, []}.
Hello.
{command,#Fun,[]}
4>
ほら、うまくいくでしょ。
実験のときは、単なる実行以外に、実行結果(関数値)を自動で表示してくれると便利だから、次のようにしておくといいかもしれません。
-module(command2).
-export([start/0]).start() ->
spawn(fun main_loop/0).main_loop() ->
receive
stop ->
ok; % 終了
{exec, Fun, Args} when is_function(Fun) ->
apply(Fun, Args), % コマンド実行
main_loop();
{print, Fun, Args} when is_function(Fun) ->
Result = apply(Fun, Args), % 実行して値を表示
io:format("Result = ~p~n", [Result]),
main_loop();
_Other -> % 無視
main_loop()
end.
やってみると:
4> c(command2).
{ok,command2}
5> Q = command2:start().<0.130.0>
6> Q!{exec, fun()->io:format("Hello.~n") end, []}.
Hello.
{exec,#Fun,[]}
7> Q!{exec, fun(X)->X*X end, [5]}. % 何も表示されない
{exec,#Fun,[5]}
8> Q!{print, fun(X)->X*X end, [5]}. % 今度は表示される
Result = 25
{print,#Fun,[5]}
9>
コマンド・タプルに関数そのものを閉じ込める代わりに、コールバックモジュール名(名前はアトム)を入れることにすれば、次のような感じです。
-module(command3).
-export([start/0]).start() ->
spawn(fun main_loop/0).main_loop() ->
receive
stop ->
ok; % 終了
{command, Mod, Arg} when is_atom(Mod) ->
Mod:exec(Arg),
main_loop();
_Other -> % 無視
main_loop()
end.
コールバックモジュールでは、exec/1 を必ず実装します。次がコールバックモジュールの例です。
-module(hello).
-export([exec/1]).exec(Arg) ->
io:format("Hello, ~s.~n", [Arg]).
やってみると:
9> c(command3).
{ok,command3}
10> c(hello).
{ok,hello}
11> R = command3:start().<0.145.0>
12> R!{command, hello, "world"}.
Hello, world.
{command,hello,"world"}
13>