Elixirでgen_statemを使ってみる


# 環境

```
$ elixir -v
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.5.1
```

# 問題

簡単な ON/OFF スイッチの状態遷移を実装します。

初期状態が OFF で、 push イベントが発生したら、 ON になる。今回はここまで。

# 初期化

``` elixir
defmodule StatemTest do
  def start_link do
    {:ok, fsm} = :gen_statem.start_link(__MODULE__, [], [])
    fsm
  end
end
```

```
iex(1)> StatemTest.start_link

18:56:31.041 [error] ** State machine #PID<0.123.0> terminating
** When server state  = :undefined
** Reason for termination = :error:{:"function not exported", {StatemTest, :init, 1}}
** Callback mode = :undefined
** Stacktrace =
**  [{:gen_statem, :init_it, 6, [file: 'gen_statem.erl', line: 676]},
 {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 247]}]

** (EXIT from #PID<0.121.0>) evaluator process exited with reason: an exception was raised:
    ** (UndefinedFunctionError) function StatemTest.init/1 is undefined or private
        (statem_test) StatemTest.init([])
        (stdlib) gen_statem.erl:676: :gen_statem.init_it/6
        (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3

Interactive Elixir (1.5.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
```

ほぉ、`init/1` が必要、と。

``` diff
 defmodule StatemTest do
   def start_link do
     {:ok, fsm} = :gen_statem.start_link(__MODULE__, [], [])
     fsm
   end
+
+  def init(args) do
+    IO.puts "called init with #{args}"
+  end
 end
```

```
iex(1)> StatemTest.start_link
called init with
** (EXIT from #PID<0.121.0>) evaluator process exited with reason: {:bad_return_from_init, :ok}

Interactive Elixir (1.5.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
18:59:03.615 [error] ** State machine #PID<0.123.0> terminating
** When server state  = :undefined
** Reason for termination = :error:{:bad_return_from_init, :ok}
** Callback mode = :undefined
** Stacktrace =
**  [{:gen_statem, :init_result, 6, [file: 'gen_statem.erl', line: 721]},
 {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 247]}]

```

お。なんか、init でいい感じの return をしてあげないといけないっぽいですね。

[`init/1` のマニュアル](http://erlang.org/doc/man/gen_statem.html#Module:init-1) を見ると、

```
Module:init(Args) -> Result(StateType)

Types

Args = term()
Result(StateType) = init_result(StateType) 
```

と書いてあって、


```
init_result(StateType) = 
    {ok, State :: StateType, Data :: data()} |
    {ok,
     State :: StateType,
     Data :: data(),
     Actions :: [action()] | action()} |
    ignore |
    {stop, Reason :: term()}
```

なので、Action を指定したり、ignore したり、stop と理由を返したりいろいろ
できるっぽいですね。今回はシンプルに `:ok` で初期状態(`:off`)を返すだけにしましょう。

``` diff
 defmodule StatemTest do
   def start_link do
     {:ok, fsm} = :gen_statem.start_link(__MODULE__, [], [])
     fsm
   end
 
   def init(args) do
     IO.puts "called init with #{args}"
+    {:ok, :off, []}
   end
 end
```

```
iex(1)> StatemTest.start_link
called init with

19:09:02.334 [error] ** State machine #PID<0.123.0> terminating
** Last event = {:internal, :init_state}
** When server state  = {:off, []}
** Reason for termination = :error:{:"function not exported", {StatemTest, :callback_mode, 0}}
** Callback mode = :undefined
** Stacktrace =
**  [{:gen_statem, :call_callback_mode, 1, [file: 'gen_statem.erl', line: 1177]},
 {:gen_statem, :enter, 7, [file: 'gen_statem.erl', line: 659]},
 {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 247]}]

** (EXIT from #PID<0.121.0>) evaluator process exited with reason: an exception was raised:
    ** (UndefinedFunctionError) function StatemTest.callback_mode/0 is undefined or private
        (statem_test) StatemTest.callback_mode()
        (stdlib) gen_statem.erl:1177: :gen_statem.call_callback_mode/1
        (stdlib) gen_statem.erl:659: :gen_statem.enter/7
        (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3

Interactive Elixir (1.5.1) - press Ctrl+C to exit (type h() ENTER for help)
```

ほほーぅ。 `callback_mode/0` という関数が必要だよ、と。

マニュアルを見ると、

```
Module:callback_mode() -> CallbackMode

Types

CallbackMode = callback_mode() | [ callback_mode() | state_enter() ] 
```

と書いてあり、さらに

```
callback_mode() = state_functions | handle_event_function
```

と書いてありますね。

- state_functions というのが状態ごとに関数を作る方式
- handle_event_function というのがイベントを処理する関数を
1つだけ作り、その中で状態とイベントに応じた処理を書く方式

という2つの書式がサポートされているようです。

今回は `state_functions` で行ってみましょう。


``` diff
 defmodule StatemTest do
   def start_link do
     {:ok, fsm} = :gen_statem.start_link(__MODULE__, [], [])
     fsm
   end
 
   def init(args) do
     IO.puts "called init with #{args}"
     {:ok, :off, []}
   end
+
+  def callback_mode do
+    :state_functions
+  end
 end
```

```
iex(1)> StatemTest.start_link
called init with
#PID<0.123.0>
iex(2)>
```

ふぉー、なんか動いたー！

# 状態の確認

今、どのステータスなのかを知りたいですね。
どうやら、 `:sys.get_status/1` とか `:sys.get_state/1` というので
調べられるらしい。

```
iex(2)> :sys.get_status(pid)
{:status, #PID<0.140.0>, {:module, :gen_statem},
 [["$ancestors": [#PID<0.121.0>, #PID<0.59.0>],
   "$initial_call": {StatemTest, :init, 1}], :running, #PID<0.121.0>, [],
  [header: 'Status for state machine <0.140.0>',
   data: [{'Status', :running}, {'Parent', #PID<0.121.0>},
    {'Logged Events', []}, {'Postponed', []}], data: [{'State', {:off, []}}]]]}
iex(3)> :sys.get_state(pid)
{:off, []}
```

ふむふむ。ちゃんと確認できましたね。

# イベントの発生と状態遷移

次は何かイベントを起こして状態遷移させてみたいですね。

ボタンを押すイメージで `push` というイベントにしてみましょうか。

イベントを発生させるのは

- `cast` : 非同期
- `call` : 同期

という方法があるらしい。この辺は `GenServer` などでお馴染みですね。

ひとまず同期でやってみましょうかね。

```
call(ServerRef :: server_ref(), Request :: term()) ->
        Reply :: term()
call(ServerRef :: server_ref(),
     Request :: term(),
     Timeout ::
         timeout() |
         {clean_timeout, T :: timeout()} |
         {dirty_timeout, T :: timeout()}) ->
        Reply :: term()
```

という 2 つの API があるらしい。後者はタイムアウトというのを
扱ってくれるようですね。まずはシンプルに前者で。

```
iex(2)> :gen_statem.call(pid, :push)

11:03:31.503 [error] ** State machine #PID<0.123.0> terminating
** Last event = {{:call, {#PID<0.121.0>, #Reference<0.3670981518.2609905667.261347>}}, :push}
** When server state  = {:off, []}
** Reason for termination = :error:{:"function not exported", {StatemTest, :off, 3}}
** Callback mode = :state_functions
** Stacktrace =
**  [{:gen_statem, :call_state_function, 5, [file: 'gen_statem.erl', line: 1240]},
 {:gen_statem, :loop_event, 6, [file: 'gen_statem.erl', line: 1012]},
 {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 247]}]

** (EXIT from #PID<0.121.0>) evaluator process exited with reason: an exception was raised:
    ** (UndefinedFunctionError) function StatemTest.off/3 is undefined or private
        (statem_test) StatemTest.off({:call, {#PID<0.121.0>, #Reference<0.3670981518.2609905667.261347>}}, :push, [])
        (stdlib) gen_statem.erl:1240: :gen_statem.call_state_function/5
        (stdlib) gen_statem.erl:1012: :gen_statem.loop_event/6
        (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
```

ほほーぅ。自動的に

- 直近のイベント
- どのステータスのときに起きたか
- なぜエラーしたのか

が表示されるのですごくわかりやすいですね。

現在 `off` というステータスに居て、 `off` で `push` ってイベントが起きたけど、
それを処理するための `off/3` という関数がないぞ、と。

マニュアルにも

>Makes a synchronous call to the gen_statem ServerRef by sending a request and waiting until its reply arrives. The gen_statem calls the state callback with event_type() {call,From} and event content Request.
>
>A Reply is generated when a state callback returns with {reply,From,Reply} as one action(), and that Reply becomes the return value of this function.

と書いてあります。

型はこんな感じ。

```
Module:StateName(enter, OldState, Data) -> StateEnterResult(StateName) 
Module:StateName(EventType, EventContent, Data) -> StateFunctionResult 
```

サンプルがだいたい後者で書かれているようなのでそれに従いましょうかね。

で、戻り値の `StateFunctionResult` は何かというと

```
event_handler_result(StateType) = 
    {next_state, NextState :: StateType, NewData :: data()} |
    {next_state,
     NextState :: StateType,
     NewData :: data(),
     Actions :: [action()] | action()} |
    state_callback_result(action())
StateType is state_name() if callback mode is state_functions, or state() if callback mode is handle_event_function.

next_state
The gen_statem does a state transition to NextState (which can be the same as the current state), sets NewData, and executes all Actions.
```

ということだそうです。今回はシンプルに
`{next_state, NextState :: StateType, NewData :: data()}` で書いてみましょう。

``` diff
 defmodule StatemTest do
   def start_link do
     {:ok, fsm} = :gen_statem.start_link(__MODULE__, [], [])
     fsm
   end
 
   def init(args) do
     IO.puts "called init with #{args}"
     {:ok, :off, []}
   end
 
   def callback_mode do
     :state_functions
   end
+
+  def off({:call, from}, :push, args) do
+    {:next_state, :on, []}
+  end
 end
```
```
iex(1)> pid = StatemTest.start_link
called init with
#PID<0.171.0>
iex(2)> :gen_statem.call(pid, :push)
```

おやぁ？このまま固まって動かなくなってしまいました。C-c で止めて、もう一度
マニュアルを読んでみましょう。

`call/3` のところにこんな説明がありました。

>A Reply is generated when a state callback returns with {reply,From,Reply} as one action(), and that Reply becomes the return value of this function.

ふむふむ。 Action を返してあげないと、 `call/3` の戻り値が生成されないのですね。
ということは 

```
{next_state, NextState :: StateType, NewData :: data()}
```

ではなくて、

```
{next_state,
     NextState :: StateType,
     NewData :: data(),
     Actions :: [action()] | action()}
```

でやらなければいけない、と。

``` diff
 defmodule StatemTest do
   def start_link do
     {:ok, fsm} = :gen_statem.start_link(__MODULE__, [], [])
     fsm
   end
 
   def init(args) do
     IO.puts "called init with #{args}"
     {:ok, :off, []}
   end
 
   def callback_mode do
     :state_functions
   end
 
   def off({:call, from}, :push, args) do
-    {:next_state, :on, []}
+    {:next_state, :on, [], {:reply, from, :on}}
   end
 end
```

```
iex(1)> pid = StatemTest.start_link
called init with
#PID<0.123.0>
iex(2)> :gen_statem.call(pid, :push)
:on
iex(3)> :sys.get_state(pid)
{:on, []}
```

おー、うまく遷移できたみたいですね。

今回はひとまずここまで。

# まとめ

- `:gen_statem.start_link` するには
  - `init/1` を定義しておかないといけない
  - `callback_mode/0` を定義しておかないといけない
- 状態を調べるには `:sys.get_state/1` や `:sys.get_status/1`
- イベントの発火は `call` か `cast`
- `:state_functions` モードのときには、状態名と同じ関数を定義し、 `:next_state` で次の状態へ遷移。
- `call` に戻り値を返すには、state function で `{:reply, from, 返答内容}` というアクションを返してあげないといけない

# 感想

- タイムアウトを扱ってたり、callback_mode なんてのを選べたり、同期・非同期が選べたり、この他にも今回述べてないけど、エラーが起きて terminate するときの振る舞いをカスタマイズできたり、とにかくいろんなところがカスタマイズできるので、フツーの状態遷移ライブラリに比べると理解するのに時間がかかる
- 型が非常にしっかり定義されてたり適切な名前がついてたりするし、それがマニュアルにちゃんと書かれているので非常にわかりやすい
- テキトーに作って動かしてみると、わかりやすいエラーが出て、次に何をしたらいいのかすぐわかる

# 気になること(次回以降？)

- 非同期 (cast) だとどうなる？
- タイムアウト
- 意図しないイベントを受け取った時の処理