Erlangと状態

Erlangにおけるアンチパターン(なのかどうか悩んでいるもの)についていくつかねたがあるので、今回はその1つ「状態を持つ機能の実現方法」について紹介します。

Erlangでは変数を書換えることはできないため、ある変数を加工する場合には新しい変数を用意することになります。

1> D = dict:new().
{dict,0,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]}}}
2> D = dict:store("Yama", "Kawa", D). ← Dと新しいDはマッチしない
** exception error: no match of right hand side value 
                    {dict,1,16,16,8,80,48,
                          {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
                          {{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],
                            [["Yama",75|...]]}}}
3> D1 = dict:store("Yama", "Kawa", D). ← 新しい変数を用意すればOK
{dict,1,16,16,8,80,48,
      {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]},
      {{[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],
        [["Yama",75|...]]}}}
4> 

dictのように内部状態がどんどん変わるようなものは、内部状態を呼出し側に戻してやって、都度それを渡してもらうようにする必要がありますが、以下のような使い勝手上の問題があります。

  • 新しい変数を都度用意するのは煩雑。うっかり古い変数を使ってしまったりするとトラブルのもと。自然とfold系の関数を多用することになり、あまり見通しがよくない。
  • 利用者側には見せる必要がない内部状態を利用者側に管理させることになる。勝手にいじられる危険性など。
  • (dictには存在しないが)内部状態を変えつつ、本来の戻り値を返したい場合にはタプルでまとめる等の工夫が必要。内部状態が変わるかどうかは利用者には関係のない話なのに、戻りの形式が変わってしまうので意識せざるを得ない。

これらの問題は、gen_serverを使えばある程度回避することができます。


(実際に使っているわけではないので動作検証はろくにしていません)

-module(dict_srv).

-behaviour(gen_server).

-export([start_link/0]).
-export([store/3, find/2]).

-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
	 terminate/2, code_change/3]).

-define(SERVER, ?MODULE). 

start_link() ->
    gen_server:start_link(?MODULE, [], []).
store(Key, Value, Pid) ->
    gen_server:cast(Pid, {store, Key, Value}).
find(Key, Pid) ->
    gen_server:call(Pid, {find, Key}).

init([]) ->
    {ok, dict:new()}.

handle_call({find, Key}, _From, D) ->
    {reply, dict:find(Key, D), D};
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

handle_cast({store, Key, Value}, D) ->
    {noreply, dict:store(Key, Value, D) };
handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

gen_serverの状態としてdictを持っておき、dictへの呼出しを中継しているだけですね。実際に使ってみましょう。

19> {ok, Pid} = dict_srv:start_link(). ← dictを作る
{ok,<0.74.0>}
20> unlink(Pid).
true
21> dict_srv:store("Yama", "Kawa", Pid). ← 1件登録
ok
22> dict_srv:find("Yama", Pid).
{ok,"Kawa"}
23> dict_srv:find("Yama?", Pid).
error

24> {ok, Pid2} = dict_srv:start_link(). ← 新しいdictを作る
{ok,<0.80.0>}
25> unlink(Pid2).
true
26> dict_srv:find("Yama", Pid2). ← Pidには存在するが、新しいdictには存在しない。
error
27> dict_srv:store("Foo", "Bar", Pid2). ← 新しいdict1件登録
ok
28> dict_srv:find("Foo", Pid2).
{ok,"Bar"}
29> dict_srv:find("Foo", Pid).
error
30> 

dictをそのまま使う場合と比較してみると、

  • storeのたびに新しい変数を用意しなくてもよくなって煩雑さは軽減
  • 内部状態を返さなくてよくなったので、戻り値も分かりやすくなった
  • 内部状態はgen_server側で隠蔽されているので、外からいじられる心配はなくなった

といったメリットが見える一方で、

  • 余計なパスが増えて性能上は不利になった
  • 本来並列で動く必要のあるものでもないのにいちいちプロセスを作るのはどうかと思う
  • gen_server側が落ちてしまうと内部状態も道連れに

など気がかりな点も出てきます。

dict程度でこんな仕掛を採用する気はないものの、状態を持ちつつもmethodがたくさんあるというというオブジェクト指向的なアプローチを、どのようにErlangで実現するのが自然なのかなあということで悩んでいます。gen_serverは大袈裟な気がするし、Parameterized Moduleはこの手の問題を解決してくれる気がしないし、かといって内部状態をユーザに渡すのは煩雑...。

と、いろいろ悩んでおり結論は出ていません。