gen_fsmのfsmとは、Finite State Machineの略です。 finite state machine は日本語で言うところの、有限状態機械(有限オートマトン)と呼ばれるものです。
詳しくは
wikipediaなりを参照してもらえば良いのですが、一言で言うならば
「複数の状態を持ち、その状態の遷移の際に何らかのアクションを伴うオブジェクトのこと」
と言えるかもしれません。
例えば、扉というオブジェクトを考えましょう。扉には2つの状態「開いている」「しまっている」という状態があります。 その遷移に伴うアクションは「開ける」「閉める」といったものです。名前は難しいのですが、意味しているところは簡単ですね。
この FSM は、サーバやネットワークの状態を表すのに都合が良いため、良く利用されます。この FSM をErlnagで実装するための ものが OTP の gen_fsm なのです。
この節では基本的な gen_fsm の利用の仕方について学びます。
以下に使用するサンプルを記載します。
-module(code_lock).
-behaviour(gen_fsm).
-export([start_link/1, button/1]).
-export([init/1]).
-export([locked/2, open/2]).
start_link(Code) ->
Rev_Code = lists:reverse(Code),
gen_fsm:start_link({local, code_lock}, code_lock, Rev_Code, []).
button(Digit) ->
gen_fsm:send_event(code_lock, {button, Digit}).
init(Code) ->
{ok, locked, {[], Code}}.
locked({button, Digit}, {SoFar, Code}) ->
case [Digit | SoFar] of
Code ->
do_unlock(),
{next_state, open, {[], Code}, 3000};
Incomplete when length(Incomplete) < length(Code) ->
io:format("please input a number 0 to 9~n",[]),
{next_state, locked, {Incomplete, Code}};
Wrong ->
io:format("~p is wrong~n",[lists:reverse(Wrong)]),
{next_state, locked, {[], Code}}
end.
open(timeout, State) ->
do_lock(),
{next_state, locked, State}.
do_unlock() ->
io:format("unlock~n",[]),
io:format("safe store is open~n",[]).
do_lock() ->
io:format("lock~n",[]).
このサンプルプログラムは簡単な金庫を模したプログラムです。
最初に4桁の数字で暗証番号を与えて、順番にその番号を押すと金庫が開かれます。
そして開かれた金庫は3秒後に閉じられます。
link_start/1
で暗証番号を与えて金庫を作成し、
button/1
で一桁ずつ数字を入力します。
実行結果
2> code_lock:start_link([1,2,3,4]).
{ok,<0.35.0>}
3> code_lock:button(1).
please input a number 0 to 9
ok
4> code_lock:button(2).
please input a number 0 to 9
ok
5> code_lock:button(3).
please input a number 0 to 9
ok
6> code_lock:button(4).
unlock
ok
safe store is open
lock
7> code_lock:button(4).
please input a number 0 to 9
ok
8> code_lock:button(4).
please input a number 0 to 9
ok
9> code_lock:button(4).
please input a number 0 to 9
ok
10> code_lock:button(4).
[4,4,4,4] is wrong
ok
gen_fsmもビヘイビアとコールバックに分離されています。上の例では、FSM は code_lock:start_link(Code)
より起動されています。
この中に gen_fsm を起動するためのビヘイビアが書かれています。
start_link(Code) ->
Rev_Code = lists:reverse(Code),
gen_fsm:start_link({local, code_lock}, code_lock, Rev_Code, []).
gen_fsm:start_link/4 が gen_fsm の起動のビヘイビアです。
この関数は次のような書式になっています。
コールバックは、指定されたモジュールの init/1 関数です。
{ok,StateName,StateData}
となります。上のサンプルでは、次のようになっています。
init(Code) ->
{ok, locked, {[], Code}}.
次の状態が "locked" で、内部に保有する値は "{[],Code}" となっています。
ここまでの実行結果は次の通りです。返り値は init/1 のものではなく、 gen_fsm:start_link/4 のものとなっている点に注意して下さい。
2> code_lock:start_link([1,2,3,4]).
{ok,<0.35.0>}
gen_fsmにおける状態遷移を行なう方法は様々なものがありますが、ここではユーザが イベントを起こすことにより状態を遷移させる方法を学びます。
イベントを起こすには、次の関数を利用します。
StateName(Event, StateData)
という関数を呼び出します。サンプルで使われているイベント
button(Digit) ->
gen_fsm:send_event(code_lock, {button, Digit}).
FSMには "状態" が存在するため、状態についての記述が必要となります。 状態の記述は、状態と同じ名前を持つ関数を適切に定義することによりなされます。
StateName(Event, StateData)
init/1 で 指定された状態である "locked" についての記述を見てみます。
locked({button, Digit}, {SoFar, Code}) ->
case [Digit | SoFar] of
Code ->
do_unlock(),
{next_state, open, {[], Code}, 3000};
Incomplete when length(Incomplete) < length(Code) ->
io:format("please input a number 0 to 9~n",[]),
{next_state, locked, {Incomplete, Code}};
Wrong ->
io:format("~p is wrong~n",[lists:reverse(Wrong)]),
{next_state, locked, {[], Code}}
end.
内部の処理は置いておいて、まず関数のヘッドと返り値に着目してみます。
ヘッドは
locked({button, Digit}, {SoFar, Code})
となっています。第一引数で受け取る値が、
gen_fsm:send_event(code_lock, {button, Digit})
の第一引数と同じものとなっています。
この関数の返り値は、
{next_state, open, {[], Code}, 3000}
{next_state, locked, {Incomplete, Code}}
{next_state, locked, {[], Code}}
のうちの一つとなります。
これは、決められた書式である必要があり、その書式は
{next_state, StateName, StateData}
となっています。タプルの第一要素はアトム next_state 、第二要素に次の状態を記し、第三要素にFSMの内部の値を記してあります。
第四要素は任意のタイムアウトの指定となっています。
簡単に動作を説明します。
金庫は今までに入力された暗唱番号(SoFar)を内部に保持しています。
暗証番号が4つに達したら、
最初に入力された暗証番号(Code)と比較します。同一の値であれば金庫を開き、状態がopen
へと遷移します。
違うのであれば入力された暗証番号をクリアし、locked
の状態へ遷移します(状態は変わらない)。
入力された値が4桁に達していないのであれば、入力された値を内部に保持し、次の桁の入力を促すため、
locked
の状態へと遷移します。
ある状態に対して、制限時間を設けることも可能です。先ほどのlocked
の中で使用されている
{next_state, open, {[], Code}, 3000}
を見て下さい。この返り値となっているタプルの第四要素がタイムアウトに指定されている時間です。
タイムアウトが指定されている状態であるopen
は次のように定義されています。
open(timeout, State) ->
do_lock(),
{next_state, locked, State}.
状態locked
はユーザからの指示であるgen_fsm:send_event/2
が
状態遷移のトリガーとなっていたのですが、タイムアウトが指定されている状態open
は
タイムアウト時間に達すると自動的に状態遷移を行ないます。
実行結果で確認してみます。
6> code_lock:button(4).
unlock
ok
safe store is open
lock
表示されている lock という文字列は、関数open
の中で利用されている、do_unlock/0
の中で表示されています。
ここから、タイムアウトにより自動的に状態遷移に伴う処理がなされているのが分かります。