とりあえずカウンターを作ってみた
とりあえずある程度覚えたらカウンターを作ってみることにしている。Perl: OOP - 西尾泰和のはてなダイアリー。JS版とかAS版もあった気がするけど見つけられなかった。
さてErlangでカウンタを作るとなると、単一代入が足かせになるように思える。
15> CounterValue = 0. 0 16> CounterValue = CounterValue + 1. ** exception error: no match of right hand side value 1
たしかにErlangの普通の変数がこういう挙動をするのは大多数の人に取っては「驚き」だとは思うので、記事とかを書く上でもこの特徴に注意を引くように書くのは理解できなくもない。でもプロセス辞書があるから単一代入はことさらに言うようなことでもないよね。
17> put(counter_value, 0). undefined 18> get(). [{counter_value,0}] 19> put(counter_value, get(counter_value) + 1). 0 20> get(). [{counter_value,1}]
みんながなんとなく「代入」で書いている処理にも、よくみると「その名前で呼ばれる値を書き換える必要のあるもの」と「たんに一時的に呼び名をつけているだけで別に書き変わらなくてもいいもの」がある。大概の言語では「書き換えられる」がデフォルトの挙動になっていて、C++なんかだと「書き換える必要がないならconstをつけなさい」と口を酸っぱくして言われる。Erlangでは最初からすべての変数がconstで、constじゃないものだけ別の記法で書くんだ。
でまあ、プロセス辞書という名前の通りこの辞書はプロセス単位なので、「1プログラム1プロセス」という頭で考えているとこれはグローバルスコープに相当する。でもErlangはプロセスのとても安い言語なのでグローバルスコープを汚染するのがいやならば新しいプロセスを作って「新しいスコープ」にしてしまえばいい。カウンターは「複数同時に動かしても干渉しない」ことが期待されるので、複数の「カウンターオブジェクト」はErlangでは複数のプロセスになる。
さて、そろそろ背景の解説は終わりにして実装の話に移ろう。実装にはgen_fsmを使った。今回は状態がcountingの1種類しかないのでgen_fsmを使う理由はあんまりなかったのだけども、使わなくてもあんまり短くなるとも思えなかったし、このコードは今後複数インスタンスを作る必要が出たときに再利用することを考えるとgen_fsmの枠組みの上で作った方が楽だと思った。ようは一言で言うと自分でreceiveとかするのめんどくさい。自分でプロセス辞書をいじるのも汚らしい。
behaviourはJavaで言うところの「Template Method」パターン。gen_fsm「クラス」を「継承」してinit/1やterminate/3などの「メソッド」を実装するとgen_fsmがそれを呼び出して一連の作業をやってくれる。括弧内はJava語。
-module(counter). -behaviour(gen_fsm). -export([start_link/1]). -export([push/1, reset/1, counting/2]). -export([init/1, terminate/3]). start_link(Name) -> gen_fsm:start_link({local, Name}, counter, 0, []). push(Name) -> gen_fsm:send_event(Name, {push, 1}). reset(Name) -> gen_fsm:send_event(Name, {reset, 0}). init(Num) -> {ok, counting, Num}. counting(Event, Data) -> case Event of {push, Num} -> NewData = Data + Num; {reset, Num} -> NewData = Num end, io:format("vaulue=~p~n", [NewData]), {next_state, counting, NewData}. terminate(_WhatsThis, _StateName, _StateData) -> ok.
こうやって使う。ちゃんと複数のインスタンスが干渉せずに動いていることがわかる。
8> counter:start_link(c1). {ok,<0.53.0>} 9> counter:push(c1). vaulue=1 ok 10> counter:push(c1). vaulue=2 ok 11> counter:start_link(c2). {ok,<0.57.0>} 12> counter:push(c2). vaulue=1 ok 13> counter:push(c1). vaulue=3 ok 14> counter:push(c2). vaulue=2 ok
複数のプロセスが立ち上がる(<0.53.0>や<0.57.0>)この個々のプロセスのPidを覚えておかなければ、そのカウンタープロセスのボタンを押すなんてことはできない。でも生のPidとか生のポインタの値みたいなもので扱いにくいので名前を付けたいので引数に名前を渡してやってその名前でregisterさせるようにしてある。
カウンタのPidは「変化する必要のない値」なので下のように書かせる設計の方が行儀がいいのかもしれない。でも面倒だよね。
{_Status, Counter1} = counter:start_link().