Help us understand the problem. What is going on with this article?

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

環境

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 というプロジェクトを作る。

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

  # 同期:5秒
  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
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 で書き換える

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
    # デフォで5秒のタイムアウトが設定される
    :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
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

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 では状態遷移の直前に標準出力している。

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

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

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

gen_fsm から gen_statem へ | blog.jxck.io

上記サイトの '## State Enter Calls' がそれか。

gen_statem で書き換える

これは楽だった。

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

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

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 を指定したら、このイベントは必須。
  # 状態遷移が起きた場合に限って1回実行される。
  # 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 を持ち出して更に突っ込んでみるか。
もしくは次の章に飛んじゃおうかしら。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした