Posted at

gen_fsmを理解する

More than 1 year has passed since last update.

Erlangの勉強のため、標準ライブラリのgen_fsmの利用例を公式サイトを見ながら写経してみました。


実装


code_lock

-module(code_lock).

-behaviour(gen_fsm).

-export([start_link/1, init/1, stop/0]).
-export([button/1]).
-export([locked/2, open/2]).
-export([code_change/4, handle_event/3, handle_info/3, handle_sync_event/4, terminate/3]).

%% process up/down
start_link(Code) ->
RevCode = lists:reverse(Code),
io:format("- start_link(~p)~n", [Code]),
gen_fsm:start_link({local, code_lock}, code_lock, RevCode, []).

init(Code) ->
io:format("- init(~p)~n", [Code]),
{ok, locked, {[], Code}}.

stop() ->
io:format("- stop~n"),
gen_fsm:send_all_state_event(code_lock, stop).

%% api
button(Digit) ->
io:format("- button(~p)~n", [Digit]),
gen_fsm:send_event(code_lock, {button, Digit}).

%% state machine
locked({button, Digit}, {SoFar, Code}) ->
io:format("- locked(~p ~p ~p)~n", [Digit, SoFar, Code]),
case [Digit | SoFar] of
Code ->
do_unlock(),
{next_state, open, {[], Code}, 3000};
Incomplate when length(Incomplate) < length(Code) ->
io:format("please input a number 0 to 9~n", []),
{next_state, locked, {Incomplate, Code}};
Wrong ->
io:format("~p is wrong~n", [lists:reverse(Wrong)]),
{next_state, locked, {[], Code}}
end.

open(timeout, State) ->
io:format("- open(~p)~n", [State]),
do_lock(),
{next_state, locked, State}.

%% gen_fsm behaviour
handle_event(Event, StateName, StateData) ->
io:format("- handle_event(~p ~p ~p)~n", [Event, StateName, StateData]),
{Event, normal, StateData}.

handle_info({Info, Pid, Reason}, StateName, StateData) ->
io:format("- handle_info(~p ~p ~p ~p ~p)~n", [Info, Pid, Reason, StateName, StateData]),
{next_state, StateName, StateData}.

handle_sync_event(A, B, C, D) ->
io:format("- handle_sync_event(~p ~p ~p ~p)~n", [A, B, C, D]),
{A, B, C, D}.

code_change(OldVsn, StateName, StateData, Extra) ->
io:format("- code_change(~p ~p ~p ~p)~n", [OldVsn, StateName, StateData, Extra]),
{ok, StateName, StateData}.

terminate(Reason, StateName, StateDate) ->
io:format("- terminate(~p ~p ~p)~n", [Reason, StateName, StateDate]),
ok.

%% local function
do_unlock() ->
io:format("- do_unlock()~n", []),
io:format("safe store is open~n", []).

do_lock() ->
io:format("- do_lock()~n", []).



使ってみる

lockを解除してみます。

% erl

Erlang/OTP 19 [erts-8.1] [source] [64-bit] [smp:4:4] [async-threads:10] [kernel-poll:false]

Eshell V8.1 (abort with ^G)
1> code_lock:start_link([1,2]).
- start_link([1,2])
- init([2,1])
{ok,<0.59.0>}
2> code_lock:button(1).
- button(1)
- locked(1 [] [2,1])
ok
please input a number 0 to 9
3> code_lock:button(2).
- button(2)
- locked(2 [1] [2,1])
ok
- do_unlock()
safe store is open
4>
4>
- open({[],[2,1]})
- do_lock()

lockを無事に解除できました。

次にわざと数字を間違えてみます。

4> code_lock:button(3).

- button(3)
- locked(3 [] [2,1])
ok
please input a number 0 to 9
5> code_lock:button(3).
- button(3)
- locked(3 [3] [2,1])
ok
[3,3] is wrong

いいですね。

最後にプロセスを終了させます。

6> code_lock:stop().

- stop
- handle_event(stop locked {[],[2,1]})
ok
- terminate(normal locked {[],[2,1]})
7>

無事終了できました。


異常系を試してみる

タイムアウト待ち中に、イベントを送ってみます。

1> code_lock:start_link([1,2]).

- start_link([1,2])
- init([2,1])
{ok,<0.59.0>}
2> code_lock:button(1).
- button(1)
- locked(1 [] [2,1])
ok
please input a number 0 to 9
3> code_lock:button(2).
- button(2)
- locked(2 [1] [2,1])
ok
- do_unlock()
safe store is open
4>
4> code_lock:button(1).
- button(1)
- terminate({function_clause,
[{code_lock,open,
[{button,1},{[],[2,1]}],
[{file,"code_lock.erl"},{line,43}]},
{gen_fsm,handle_msg,7,[{file,"gen_fsm.erl"},{line,451}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,247}]}]} open {[],[2,1]})
ok
5>
=ERROR REPORT==== 7-Nov-2016::18:26:08 ===
** State machine code_lock terminating
** Last event in was {button,1}
** When State == open
** Data == {[],[2,1]}
** Reason for termination =
** {function_clause,[{code_lock,open,
[{button,1},{[],[2,1]}],
[{file,"code_lock.erl"},{line,43}]},
{gen_fsm,handle_msg,7,[{file,"gen_fsm.erl"},{line,451}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,247}]}]}
** exception error: no function clause matching code_lock:open({button,1},{[],[2,1]}) (code_lock.erl, line 43)
in function gen_fsm:handle_msg/7 (gen_fsm.erl, line 451)
in call from proc_lib:init_p_do_apply/3 (proc_lib.erl, line 247)

open({button, Digit}, State) がマッチせずクラッシュしました。

定義を追加します。

open(timeout, State) ->

io:format("- open(~p)~n", [State]),
do_lock(),
{next_state, locked, State};
open({button, Digit}, State) -> io:format("- call button in open(~p)~n", [Digit]),
{next_state, locked, State}.

もう一度同じことをしてみます。

1> c(code_lock).

2> code_lock:start_link([1,2]).
- start_link([1,2])
- init([2,1])
{ok,<0.64.0>}
3> code_lock:button(1).
- button(1)
- locked(1 [] [2,1])
ok
please input a number 0 to 9
4> code_lock:button(2).
- button(2)
- locked(2 [1] [2,1])
ok
- do_unlock()
safe store is open
5>
5> code_lock:button(1).
- button(1)
- call button in open(1)
ok

クラッシュしなくなりました。


まとめ

timeoutを指定できる点は便利そうでした。

ただパターンマッチしないとクラッシュしてしまう点がコードを見るだけでは直感的に分かりにくいような気もします。今回の状態はlockedとopenの2つとシンプルですが、写経している時はopen状態でbuttonイベントが来たらクラッシュするという点に気づきませんでした。

ただErlang 19.0からはgen_fsmの代わりにgen_statemが新しく使いやすいという事ですので、次はこちらもみてみたいです。