環境
$ 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
というプロジェクトを作る。
$ mix new fsm --module FSM
...
$ cd fsm
$ mkdir lib/fsm
$ touch lib/fsm/cat.ex lib/fsm/dog.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
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
で書き換える
猫
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(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, "にゃー"}
これだけでもだいぶ理解が進む。
犬
犬の場合は綺麗にいかない。
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(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
状態遷移の直後
状態遷移直後にアクションを起こしたい場合はどうすればいいのだろう?
公式のドキュメントを眺めてみたけど、これといったものは無かった。
bark
は定期的にループしているのでタイマーを使えば良さそうだが、
wag_tail
のタイムアウトは1度だけ、sit
はタイムアウトを持たない。
この柔軟性の無さで非推奨になった?
じゃー gen_statem
ならできるのかな?
gen_fsm から gen_statem へ | blog.jxck.io
上記サイトの '## State Enter Calls' がそれか。
※ gen_statem
で書き換える
猫
これは楽だった。
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
犬
こっちはきつかったぞー。
ドキュメントの意味を読み取るのに一苦労だった。
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 を持ち出して更に突っ込んでみるか。
もしくは次の章に飛んじゃおうかしら。