Elixir でステートマシンを処理
「Akka でステートマシンを処理」 と同じ処理を gen_statem
の Elixir 用ラッパー(以下)を使って実装します。
ソースは http://github.com/fits/try_samples/tree/master/blog/20171121/
ステートマシンの実装(sample1)
まず、以下のステートマシンを実装します。
- 初期状態は Idle 状態
- Idle 状態で On イベントが発生すると Active 状態へ遷移
- Active 状態で Off イベントが発生すると Idle 状態へ遷移
現在の状態 | Off | On |
---|---|---|
Idle | Active | |
Active | Idle |
準備
mix でプロジェクトを作成します。
プロジェクト作成
> mix new sample1 ・・・ > cd sample1
mix.exs の deps へ gen_state_machine
を追加します。
今回は escript で実行するので、そのための設定も追加しておきます。
mix.exs
defmodule Sample1.Mixfile do use Mix.Project def project do [ ・・・ deps: deps(), # escript の設定 escript: [ main_module: Sample1 ] ] end ・・・ defp deps do [ # GenStateMachine {:gen_state_machine, "~> 2.0"} ] end end
実装
handle_event(event_type, event_content, state, data)
関数でイベントをハンドリングし、{:next_state, <遷移先の状態>, <新しいデータ>}
を返せば新しい状態へ遷移します。
状態遷移しない場合は {:keep_state, <データ>}
(:keep_state_and_data
でも可)を返します。
ここで、cast
関数(返事を待たない一方通行の呼び出し。Akka の tell と同じ)を使った場合のイベントタイプは :cast
となります。
lib/sample_state_machine.ex
defmodule SampleStateMachine do use GenStateMachine # 初期状態 def init(_args) do {:ok, :idle, 0} end # on イベントの処理(idle から active へ遷移) def handle_event(:cast, :on, :idle, data) do IO.puts "*** :on, idle -> active" {:next_state, :active, data + 1} end # off イベントの処理(active から idle へ遷移) def handle_event(:cast, :off, :active, data) do IO.puts "*** :off, active -> idle" {:next_state, :idle, data} end # 上記以外 def handle_event(event_type, event_content, state, data) do IO.puts "*** Unhandled: type=#{event_type}, content=#{event_content}, state=#{state}, data=#{data}" {:keep_state, data} # 以下でも可 # {:keep_state_and_data, []} end end
このステートマシンを動作確認するための処理を実装します。
escript で実行できるように main
関数内に定義しています。
lib/sample1.ex
defmodule Sample1 do def main(_args) do {:ok, pid} = GenStateMachine.start_link(SampleStateMachine, nil) GenStateMachine.cast(pid, :on) GenStateMachine.cast(pid, :off) GenStateMachine.cast(pid, :off) GenStateMachine.stop(pid) end end
ビルドと実行
deps.get
で gen_state_machine を取得します。
依存パッケージの取得
> mix deps.get Running dependency resolution... Dependency resolution completed: gen_state_machine 2.0.1 * Getting gen_state_machine (Hex package) Checking package (https://repo.hex.pm/tarballs/gen_state_machine-2.0.1.tar) Using locally cached package
escript.build
で escript 実行用にビルドします。
ビルド
> mix escript.build ==> gen_state_machine Compiling 3 files (.ex) Generated gen_state_machine app ==> sample1 Compiling 2 files (.ex) Generated sample1 app Generated escript sample1 with MIX_ENV=dev
escript コマンドで実行します。
実行結果
> escript sample1 *** :on, idle -> active *** :off, active -> idle *** Unhandled: type=cast, content=off, state=idle, data=1
タイムアウト付きステートマシンの実装(sample2)
次に、タイムアウト時の遷移を追加してみます。
現在の状態 | Off | On | Timeout (2秒) |
---|---|---|---|
Idle | Active | ||
Active | Idle | Idle |
実装
{:next_state, <遷移先の状態>, <新しいデータ>, <タイムアウト(ミリ秒)>}
を返すとタイムアウトを設定できます。
イベントタイプ :timeout
でタイムアウトをハンドリングできます。
注意点として、このタイムアウトは状態自体のタイムアウトではなくイベントの受信に対するタイムアウトです。
lib/timeout_state_machine.ex
defmodule TimeoutStateMachine do use GenStateMachine def init(_args) do {:ok, :idle, 0} end def handle_event(:cast, :on, :idle, data) do IO.puts "*** :on, idle -> active" # 2秒タイムアウト {:next_state, :active, data + 1, 2000} end def handle_event(:cast, :off, :active, data) do IO.puts "*** :off, active -> idle" {:next_state, :idle, data} end # タイムアウト時の処理 def handle_event(:timeout, event_content, :active, data) do IO.puts "*** :timeout content=#{event_content}, active -> idle" {:next_state, :idle, data} end def handle_event(event_type, event_content, state, data) do IO.puts "*** Unhandled: type=#{event_type}, content=#{event_content}, state=#{state}, data=#{data}" {:keep_state, data} end end
動作確認の処理を実装します。
lib/sample2.ex
defmodule Sample2 do def main(_args) do {:ok, pid} = GenStateMachine.start_link(TimeoutStateMachine, nil) GenStateMachine.cast(pid, :on) GenStateMachine.cast(pid, :off) GenStateMachine.cast(pid, :off) GenStateMachine.cast(pid, :on) :timer.sleep(2500) GenStateMachine.cast(pid, :on) :timer.sleep(1500) GenStateMachine.cast(pid, :invalid_message) :timer.sleep(1500) GenStateMachine.cast(pid, :invalid_message) :timer.sleep(2500) GenStateMachine.stop(pid) end end
ビルドと実行
依存パッケージの取得とビルド
> mix deps.get ・・・ > mix escript.build ・・・
実行結果は以下の通り、invalid_message のハンドリングでタイムアウトは機能しなくなっています。
実行結果
> escript sample2 *** :on, idle -> active *** :off, active -> idle *** Unhandled: type=cast, content=off, state=idle, data=1 *** :on, idle -> active *** :timeout content=2000, active -> idle *** :on, idle -> active *** Unhandled: type=cast, content=invalid_message, state=active, data=3 *** Unhandled: type=cast, content=invalid_message, state=active, data=3
状態タイムアウト付きステートマシンの実装(sample3)
最後に、状態のタイムアウトを実現します。
実装
:next_state
を返す際に {:state_timeout, <タイムアウト(ミリ秒)>, <イベント>}
を設定したリストを含める事で状態のタイムアウトを実現できます。
状態のタイムアウトはイベントタイプ :state_timeout
でハンドリングします。
lib/state_timeout_state_machine.ex
defmodule StateTimeoutStateMachine do use GenStateMachine def init(_args) do {:ok, :idle, 0} end def handle_event(:cast, :on, :idle, data) do IO.puts "*** :on, idle -> active" # 状態タイムアウトの設定 actions = [{:state_timeout, 2000, :off}] {:next_state, :active, data + 1, actions} end def handle_event(:cast, :off, :active, data) do IO.puts "*** :off, active -> idle" {:next_state, :idle, data} end # 状態タイムアウトの処理 def handle_event(:state_timeout, :off, :active, data) do IO.puts "*** :state_timeout, active -> idle" {:next_state, :idle, data} end def handle_event(event_type, event_content, state, data) do IO.puts "*** Unhandled: type=#{event_type}, content=#{event_content}, state=#{state}, data=#{data}" {:keep_state, data} end end
動作確認の処理を実装します。
lib/sample3.ex
defmodule Sample3 do def main(_args) do {:ok, pid} = GenStateMachine.start_link(StateTimeoutStateMachine, nil) GenStateMachine.cast(pid, :on) GenStateMachine.cast(pid, :off) GenStateMachine.cast(pid, :off) GenStateMachine.cast(pid, :on) :timer.sleep(2500) GenStateMachine.cast(pid, :on) :timer.sleep(1500) GenStateMachine.cast(pid, :invalid_message) :timer.sleep(1500) GenStateMachine.cast(pid, :invalid_message) :timer.sleep(2500) GenStateMachine.stop(pid) end end
ビルドと実行
依存パッケージの取得とビルド
> mix deps.get ・・・ > mix escript.build ・・・
実行結果は以下の通り、invalid_message のハンドリングとは無関係に状態のタイムアウトが機能しています。
実行結果
> escript sample3 *** :on, idle -> active *** :off, active -> idle *** Unhandled: type=cast, content=off, state=idle, data=1 *** :on, idle -> active *** :state_timeout, active -> idle *** :on, idle -> active *** Unhandled: type=cast, content=invalid_message, state=active, data=3 *** :state_timeout, active -> idle *** Unhandled: type=cast, content=invalid_message, state=idle, data=3