すごいE本 第15章 on Elixir （有限ステートマシン）

# 環境

```shell-session:sh
$ lsb_release -d
Description:    Ubuntu 18.04.2 LTS

$ elixir -v
Erlang/OTP 21 [erts-10.3.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Elixir 1.8.1 (compiled with Erlang/OTP 20)
```

そう遠くない昔、Elixir には `GenFSM` が存在した。
それは廃止され、新たに登場したのが `Agent` である。（代替え案ではない）
本家 Erlang では `gen_fsm` は非推奨となり、
新たに登場したのが `gen_statem` である。

まずは E本の内容を理解するために `gen_fsm` で書き、
それを `gen_statem` でリファクタリングしてみようと思う。

まぢで？
いや、ほんと、しんどい。

# 15.1 有限ステートマシンとは何か

`FSM` というプロジェクトを作る。

```shell-session:sh
$ mix new fsm --module FSM
...
$ cd fsm
$ mkdir lib/fsm
$ touch lib/fsm/cat.ex lib/fsm/dog.ex
```
```elixir:fsm/lib/fsm/cat.ex
defmodule FSM.Cat do
  def start, do: spawn(&dont_give_crap/0)

  # 同期：５秒
  def event(pid, event) do
    ref = make_ref()
    send(pid, {self(), ref, event})

    receive do
      {^ref, msg} -> {:ok, msg}
    after
      5_000 -> {:error, :timeout}
    end
  end

  # 猫の状態を表す関数
  defp dont_give_crap do
    receive do
      {pid, ref, _msg} -> send(pid, {ref, "にゃー"})
      _ -> :ok
    end

    IO.puts("Switching to 'ガラクタを与えないで' state")
    dont_give_crap()
  end
end
```
```elixir:fsm/lib/fsm/dog.ex
defmodule FSM.Dog do
  def start, do: spawn(&bark/0)

  # 非同期
  def squirrel(pid), do: send(pid, :squirrel)

  # 非同期
  def pet(pid), do: send(pid, :pet)

  ###
  # 状態

  defp bark do
    IO.puts("Dog says: わんわん！")

    receive do
      :pet ->
        wag_tail()

      _ ->
        IO.puts("犬は混乱している。")
        bark()
    after
      2_000 -> bark()
    end
  end

  defp wag_tail do
    IO.puts("犬は尻尾を振っている。")

    receive do
      :pet ->
        sit()

      _ ->
        IO.puts("犬は混乱している。")
        wag_tail()
    after
      30_000 -> bark()
    end
  end

  defp sit do
    IO.puts("犬は座っている。なんていい子なんでしょう！")

    receive do
      :squirrel ->
        bark()

      _ ->
        IO.puts("犬は混乱している。")
        sit()
    end
  end
end
```

まー、このぐらいはわかる。
で、この後、`gen_fsm` を使う例が展開されていくんだけど、
「どうした？」ってぐらい急勾配に駆け上がっていくんだよね。
正直まったく頭に入ってこない。

ここはジックリと猫・犬に腰を据えてみる。

## ※ `gen_fsm` で書き換える
### 猫

```elixir:fsm/lib/fsm/cat2.ex
defmodule FSM.Cat2 do
  alias __MODULE__, as: Me

  # モジュール、初期引数、オプションで呼び出す。
  def start, do: :gen_fsm.start(Me, [], [])

  # 同期
  def event(pid, event) do
    # デフォで５秒のタイムアウトが設定される
    :gen_fsm.sync_send_event(pid, event)
  end

  ###
  # Callbacks

  # {:ok, 次状態コールバック、状態データ}
  def init([]), do: {:ok, :dont_give_crap, []}

  # 状態コールバック（任意名）
  def dont_give_crap(_event, _from, []) do
    IO.puts("Switching to 'ガラクタを与えないで' state")
    {:reply, {:ok, "にゃー"}, :dont_give_crap, []}
  end
end
```
```elixir:iex
iex(1)> {:ok, cat} = FSM.Cat2.start()
{:ok, #PID<0.144.0>}
iex(2)> FSM.Cat2.event(cat, :pet)
Switching to 'ガラクタを与えないで' state
{:ok, "にゃー"}
iex(3)> FSM.Cat2.event(cat, :love)
Switching to 'ガラクタを与えないで' state
{:ok, "にゃー"}
iex(4)> FSM.Cat2.event(cat, :cherish)
Switching to 'ガラクタを与えないで' state
{:ok, "にゃー"}
```

これだけでもだいぶ理解が進む。

### 犬

犬の場合は綺麗にいかない。

![FSM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105190/7928673e-84b2-093c-dcf7-6a20869c1012.png)

```elixir:fsm/lib/fsm/dog2.ex
defmodule FSM.Dog2 do
  alias __MODULE__, as: Me

  def start, do: :gen_fsm.start(Me, [], [])

  # 非同期
  def squirrel(pid), do: :gen_fsm.send_event(pid, :squirrel)

  # 非同期
  def pet(pid), do: :gen_fsm.send_event(pid, :pet)

  ###
  # Callbacks

  @timeout_bark 2_000
  @timeout_wag 30_000

  def init([]) do
    IO.puts("Dog says: わんわん！")

    # 意味：時限付きでわんわん状態に遷移してね
    # call -> bark(:timeout, data)
    {:ok, :bark, [], @timeout_bark}
  end

  def bark(:timeout, []) do
    IO.puts("Dog says: わんわん！")
    {:next_state, :bark, [], @timeout_bark}
  end

  def bark(:pet, []) do
    IO.puts("犬は尻尾を振っている。")
    {:next_state, :wag_tail, [], @timeout_wag}
  end

  def bark(_event, []) do
    IO.puts("犬は混乱している。")
    IO.puts("Dog says: わんわん！")
    {:next_state, :bark, [], @timeout_bark}
  end

  def wag_tail(:timeout, []) do
    IO.puts("Dog says: わんわん！")
    {:next_state, :bark, [], @timeout_bark}
  end

  def wag_tail(:pet, []) do
    IO.puts("犬は座っている。なんていい子なんでしょう！")
    {:next_state, :sit, []}
  end

  def wag_tail(_event, []) do
    IO.puts("犬は混乱している。")
    IO.puts("犬は尻尾を振っている。")
    {:next_state, :wag_tail, [], @timeout_wag}
  end

  def sit(:squirrel, []) do
    IO.puts("Dog says: わんわん！")
    {:next_state, :bark, [], @timeout_bark}
  end

  def sit(_event, []) do
    IO.puts("犬は混乱している。")
    IO.puts("犬は座っている。なんていい子なんでしょう！")
    {:next_state, :sit, []}
  end
end
```

`Dog` では状態遷移の直後に標準出力するが、
`Dog2` では状態遷移の直前に標準出力している。

```elixir:iex
iex(1)> alias FSM.Dog2
FSM.Dog2
iex(2)> {:ok, dog} = Dog2.start
Dog says: わんわん！
{:ok, #PID<0.145.0>}
Dog says: わんわん！
Dog says: わんわん！
Dog says: わんわん！
iex(3)> Dog2.pet(dog)
犬は尻尾を振っている。
:ok
iex(4)> Dog2.pet(dog)
犬は座っている。なんていい子なんでしょう！
:ok
iex(5)> Dog2.pet(dog)
犬は混乱している。
:ok
犬は座っている。なんていい子なんでしょう！
iex(6)> Dog2.squirrel(dog)
Dog says: わんわん！
:ok
Dog says: わんわん！
iex(7)> Dog2.pet(dog)
犬は尻尾を振っている。
:ok
# 30秒待機する
Dog says: わんわん！
Dog says: わんわん！
Dog says: わんわん！
iex(8)> Dog2.pet(dog)
犬は尻尾を振っている。
:ok
iex(9)> Dog2.pet(dog)
犬は座っている。なんていい子なんでしょう！
:ok
```

### 状態遷移の直後

状態遷移直後にアクションを起こしたい場合はどうすればいいのだろう？
公式のドキュメントを眺めてみたけど、これといったものは無かった。

[Erlang -- gen_fsm](http://erlang.org/documentation/doc-6.1/lib/stdlib-2.1/doc/html/gen_fsm.html)

`bark` は定期的にループしているのでタイマーを使えば良さそうだが、
`wag_tail` のタイムアウトは１度だけ、`sit` はタイムアウトを持たない。

この柔軟性の無さで非推奨になった？
じゃー `gen_statem` ならできるのかな？

[gen_fsm から gen_statem へ | blog.jxck.io](https://blog.jxck.io/entries/2017-05-18/gen_statem.html)

上記サイトの '[## State Enter Calls](https://blog.jxck.io/entries/2017-05-18/gen_statem.html#state-enter-calls)' がそれか。

## ※ `gen_statem` で書き換える
### 猫

これは楽だった。

```elixir:fsm/lib/fsm/cat3.ex
defmodule FSM.Cat3 do
  alias __MODULE__, as: Me

  def start, do: :gen_statem.start(Me, [], [])

  def event(pid, event), do: :gen_statem.call(pid, event)

  ###
  # Callbacks

  # gen_fsm ライクなスタイルを指定
  def callback_mode, do: :state_functions

  def init([]), do: {:ok, :dont_give_crap, []}

  def dont_give_crap({:call, from}, _event, data) do
    IO.puts("Switching to 'ガラクタを与えないで' state")
    action = {:reply, from, {:ok, "にゃー"}}
    {:next_state, :dont_give_crap, data, action}
  end
end
```

### 犬

こっちはきつかったぞー。
ドキュメントの意味を読み取るのに一苦労だった。

```elixir:fsm/lib/fsm/dog3.ex
defmodule FSM.Dog3 do
  alias __MODULE__, as: Me

  def start, do: :gen_statem.start(Me, [], [])

  def squirrel(pid), do: :gen_statem.cast(pid, :squirrel)

  def pet(pid), do: :gen_statem.cast(pid, :pet)

  ###
  # Callbacks

  # パターンマッチ・スタイル＆状態入口アクションに指定
  def callback_mode, do: [:handle_event_function, :state_enter]

  def init([]), do: {:ok, :bark, []}

  @timeout_bark 2_000
  @timeout_wag 30_000
  @immediately 0

  ## bark
  # callback_mode: :state_enter を指定したら、このイベントは必須。
  # 状態遷移が起きた場合に限って１回実行される。
  # init/1 から入ってきた場合、old_state == :bark
  def handle_event(:enter, _old_state, :bark, data) do
    IO.puts("Dog says: わんわん！")
    {:next_state, :bark, data, @timeout_bark}
  end

  # context には呼出元で指定したタイムアウト値が入っている。
  # ここでわんわんループ
  def handle_event(:timeout, _context, :bark, data) do
    IO.puts("Dog says: わんわん！")
    {:next_state, :bark, data, @timeout_bark}
  end

  def handle_event(:cast, :pet, :bark, data) do
    {:next_state, :wag_tail, data}
  end

  def handle_event(:cast, _event, :bark, data) do
    IO.puts("犬は混乱している。")
    {:next_state, :bark, data, @immediately}
  end

  ## wag_tail
  def handle_event(:enter, _old_state, :wag_tail, data) do
    IO.puts("犬は尻尾を振っている。")
    {:next_state, :wag_tail, data, @timeout_wag}
  end

  def handle_event(:timeout, @timeout_wag, :wag_tail, data) do
    {:next_state, :bark, data}
  end

  def handle_event(:cast, :pet, :wag_tail, data) do
    {:next_state, :sit, data}
  end

  def handle_event(:cast, _event, :wag_tail, data) do
    IO.puts("犬は混乱している。")
    IO.puts("犬は尻尾を振っている。")
    {:next_state, :wag_tail, data, @timeout_wag}
  end

  ## sit
  def handle_event(:enter, _old, :sit, data) do
    IO.puts("犬は座っている。なんていい子なんでしょう！")
    {:next_state, :sit, data}
  end

  def handle_event(:cast, :squirrel, :sit, data) do
    {:next_state, :bark, data}
  end

  def handle_event(:cast, _event, :sit, data) do
    IO.puts("犬は混乱している。")
    {:next_state, :sit, data}
  end
end
```

# 次回

ちょっと休憩。
この後どーしよーかなー。
E本のアイテム取引を書き換えるか、Agent を持ち出して更に突っ込んでみるか。
もしくは次の章に飛んじゃおうかしら。