gen_fsmを理解する


Erlangの勉強のため、標準ライブラリのgen_fsmの利用例を[公式サイト](http://erlang.org/doc/man/gen_fsm.html)を見ながら写経してみました。

## 実装

```erlang: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を解除してみます。

```text
% 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を無事に解除できました。
次にわざと数字を間違えてみます。

```tex
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
```

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

```text
6> code_lock:stop().
- stop
- handle_event(stop locked {[],[2,1]})
ok
- terminate(normal locked {[],[2,1]})
7> 
```

無事終了できました。

## 異常系を試してみる
タイムアウト待ち中に、イベントを送ってみます。

```txt
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) がマッチせずクラッシュしました。
定義を追加します。

```erlang
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}.
```

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

```txt
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](http://erlang.org/doc/design_principles/statem.html)が新しく使いやすいという事ですので、次はこちらもみてみたいです。