Erlang World


top > otp > gen_fsm

gen_fsm

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の起動

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 の起動のビヘイビアです。
この関数は次のような書式になっています。

start_link(Module, Args, Options) -> Result
start_link(FsmName, Module, Args, Options) -> Result
FsmName = {local,Name} | {global,GlobalName}
Name = atom()
GlobalName = term()
Module = atom()
Args = term()
Options = [Option]
Option = {debug,Dbgs} | {timeout,Time} | {spawn_opt,SOpts}
Result = {ok,Pid} | ignore | {error,Error}
Pid = pid()
Error = {already_started,Pid} | term()
FsmNameは登録する名前、Moduleはコールバックの実装モジュール、 Argsはコールバックに渡される引数、Optionsはオプションとなっています。

コールバックは、指定されたモジュールの init/1 関数です。

Module:init(Args) -> Result
Args = term()
Result = {ok,StateName,StateData} | {ok,StateName,StateData,Timeout}
| {stop,Reason} | ignore
StateName = atom()
StateData = term()
Timeout = int()>0 | infinity
Reason = term()
Resultは指定された書式を記す必要があります。通常は、 {ok,StateName,StateData} となります。
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における状態遷移を行なう方法は様々なものがありますが、ここではユーザが イベントを起こすことにより状態を遷移させる方法を学びます。

イベントを起こすには、次の関数を利用します。

send_event(FsmRef, Event) -> ok
FsmRef = Name | {Name,Node} | {global,GlobalName} | pid()
Name = Node = atom()
GlobalName = term()
Event = term()
Nameは FSM として登録した名前です。Eventは指定された gen_fsm に対して送られるメッセージです。 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の中で表示されています。
ここから、タイムアウトにより自動的に状態遷移に伴う処理がなされているのが分かります。


Yuichi ITO. All rights reserved.
mail to : ad
inserted by FC2 system