この章は、すべてのインタフェース関数、コールバック関数が詳細に説明されている、gen_fsm(3)と合わせて読んでください。
有限ステートマシン(FSM: Finite State Machine, 有限状態機械)は次の形式で示される関係の集合として表すことができます。
ステート(S) x イベント(E) -> アクション(A), 状態(S')
これらの関係は次のように解釈することができます
もしも、ステートSの時に、イベントEが起きると、アクションAが実行され、
次のステートS'に移行する。
FSMの実装にはgen_fsmビヘイビアを使い、状態のトランジションのルールは次のような形式のErlangの関数として実装します。
ステート名(イベント, 状態データ) ->
.. アクションのコードをここに書く ...
{次のステート, ステート名', ステートデータ'}
FSMのサンプルとして、コードによってロックがかけられるドアについて見ていきます。初期状態では、ドアはロックされています。誰かがボタンを押すと、イベントが発生します。どのボタンが以前に押されたのかによって、操作シーケンスが、正しい、まだ完了していない、間違い、というどれかの状態になります。
正しい操作が行われたら、ドアは30秒間(30,000ミリ秒)だけ解錠されます。もし、まだ完了していない場合には、次のボタン操作を待ちます。もし間違ってしまうと、スタートに戻り、新しいボタンの操作のシーケンスを待ちます。
コードロックFSMは、gen_fsmを用いてコールバックモジュールとして実装します。
-module(code_lock).
-behaviour(gen_fsm).
-export([start_link/1]).
-export([button/1]).
-export([init/1, locked/2, open/2]).
start_link(Code) ->
gen_fsm:start_link({local, code_lock}, code_lock, 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) ->
{next_state, locked, {Incomplete, Code}};
_Wrong ->
{next_state, locked, {[], Code}}
end.
open(timeout, State) ->
do_lock(),
{next_state, locked, State}.
コードの解説は次のセクションで行います。
前のセクションのサンプルでは、まず、 code_lock:start_link(Code) が呼ばれるところから、 gen_fsm の動作がスタートします。
start_link(Code) ->
gen_fsm:start_link({local, code_lock}, code_lock, Code, []).
start_link は gen_fsm:start_link/4 を呼び出します。この関数はプロセスをspawnして、 gen_fsm と新しいプロセスを結びつけます。
最初の引数の {local, code_lock} は名前を設定します。この場合、ローカルで code_lock という名前で gen_fsm が登録されます。
名前が省略されると、この gen_fsm は登録されません。代わりに、pidが使用されます。名前としては、 {global, 名前} という指定もできます。この場合、 gen_fsm は global:register_name/2 を使って登録されます。
2つ目の引数の code_lock は、コールバックモジュールの名前を表します。このモジュールは、コールバック関数が置かれているモジュールになります。
この場合、インタフェース関数(start_link と button)は、コールバック関数(init, locked, open)同じモジュール内に置かれています。これは通常は良いプログラミングのプラクティスです。一つのプレセスに関連するコードは一つのモジュールにすべきです。
もし名前の登録が成功すると、新しいgen_fsmプロセスは、コールバック関数として、 code_lock:init(Code) という呼び出しを行います。この関数は {ok, ステート名, ステートデータ} という値を返すことが期待されています。 ステート名 はgen_fsmの初期のステートの名前です。この場合は、ドアは最初は鍵がかかっているという想定で、 locked になっています。 ステートデータ は、gen_fsmの内部のステートです。gen_fsmの場合、ステートマシンのステートと区別するために、内部のステートは「ステートデータ」と呼びます。この場合、ステートデータには、今まで押されたボタンの順序(最初は空)と、現在の暗証番号を含んでいます。
init(Code) ->
{ok, locked, {[], Code}}.
gen_fsm:start_link は同期実行されます。この関数はgen_fsmの初期化が完了し、通知を受け取る準備ができるまでは返りません。
gen_fsm:start_link は、スーパーバイザによって起動され、gen_fsmをスーパービジョンツリーの一部として使う場合にのみ使用してください。スーパービジョンツリーとは独立し、スタンドアローンのgen_fsmとして使う場合には、 gen_fsm:start という別の関数があります。
3.4 Notifying About Events
button(Digit) ->
gen_fsm:send_event(code_lock, {button, Digit}).
code_lock はgen_fmsの名前で、その名前を使って開始されるということに同意する必要があります。
イベントは、メッセージとして処理されて、gen_fsmに送られます。イベントを受信すると、gen_fsmは ステート名(イベント, ステートデータ) という名前で関数を呼び出します。この関数は、 {next_state, ステート名1, ステートデータ1} というタプルを返さなければなりません。この説明例の中の「ステート名」は現在のステート、「ステート名1」は次に進むステートの名前です。「ステートデータ1」はgen_fsmが持つ、新しいステートデータです。
locked({button, Digit}, {SoFar, Code}) ->
case [Digit|SoFar] of
Code ->
do_unlock(),
{next_state, open, {[], Code}, 30000};
Incomplete when length(Incomplete)<length(Code) ->
{next_state, locked, {Incomplete, Code}};
_Wrong ->
{next_state, locked, {[], Code}};
end.
open(timeout, State) ->
do_lock(),
{next_state, locked, State}.
ドアがロックされていて、ボタンが押されると、今までの押されたボタンのシーケンスと、正しい解除コードを比較します。結果次第で、鍵を解除してgen_fsmが open というステートに移動したり、 locked のままのステートに居続けます。
もし、ただしいコードが与えられている時に、鍵が解除されたときに、 locked/2 は次のようなタプルを返しています。
{next_state, open, {[], Code}, 30000};
30000はミリ秒単位の、タイムアウト時間を表しています。30000ミリ秒、つまり30秒経つと、タイムアウトが発生し、 ステート名(timeout, ステートデータ) が呼ばれます。この場合、ドアの状態は30秒間だけ open になり、その後タイムアウトが発生します。その後ドアは再び施錠されます。
open(timeout, State) ->
do_lock(),
{next_state, locked, State}.
イベントは、gen_fsmのあらゆるステート時に送ることができます。 gen_fsm:send_event/2 を使い、ステート関数をそれぞれ作ってイベントを取り扱う方法もありますし、 gen_fsm:send_all_state_envet/2 を使って送信し、 モジュール:handle_event/3 を作ってイベントを取り扱うこともできます。
-module(code_lock).
...
-export([stop/0]).
...
stop() ->
gen_fsm:send_all_state_event(code_lock, stop).
...
handle_event(stop, _StateName, StateData) ->
{stop, normal, StateData}.
もし、gen_fsmを監視ツリーの中で動かすのであれば、終了関数は不要です。監視ツリーが自動的にgen_fsmを終了させます。正確には、スーパバイザの shutdown_strategy を定義することで作業が完了します。
終了前に片付けが必要であれば、シャットダウン戦略にタイムアウト値を設定し、 init 関数の中で、gen_fsmの終了シグナルを捕まえる設定をしなければなりません。シャットダウンの指令が来ると、 gen_fsm は terminal(shutdown, ステート名、ステートデータ) という形式で、コールバック関数を呼び出します。
init(Args) ->
...,
process_flag(trap_exit, true),
...,
{ok, StateName, StateData}.
...
terminate(shutdown, StateName, StateData) ->
..片付けコードをここに書く..
ok.
gen_fsmが監視ツリーの一部でない場合には、 stop 関数が便利です。次のコードがサンプルになります。
...
-export([stop/0]).
...
stop() ->
gen_fsm:send_all_state_event(code_lock, stop).
...
handle_event(stop, _StateName, StateData) ->
{stop, normal, StateData}.
...
terminate(normal, _StateName, _StateData) ->
ok.
このコールバック関数は終了イベント時に呼ばれ、 {stop, normal, ステートデータ} というタプルを返します。 normal は正常終了を表し、 ステートデータ はgen_fsmのステートデータです。これにより、gen_fsmは terminate(normal,StateName,StateData1) 優雅に終了します。
もしgen_fsmが、イベント以外の他のメッセージも受け取れるようにしたいと考えているのであれば、これを取り扱うために handle_info(情報、ステート名、ステートデータ) というコールバックファンクションを実装します。他のメッセージが「終了」で、gen_fsmがスーパバイザではなく、他のプロセスとリンクしている場合、終了イベントをトラップできます。
handle_info({'EXIT', Pid, Reason}, StateName, StateData) ->
..終了時のコード..
{next_state, StateName1, StateData1}.
Copyright (c) 1991-2009 Ericsson AB