LoginSignup
7
2

More than 5 years have passed since last update.

はじめてなElixir(9) 有限状態機械を作ってみる

Last updated at Posted at 2018-09-22

有限状態マシン(finite state machine: FSM)をクリアしましょ。とにかくクリアしましょ。

関数型パラダイムとFSM

はじめてなElixir(7)に書いたことの繰り返しです。「関数型言語でFSMってどうよ」って思ってます。

FSM用のモジュール

とわいえ、表現できないことには仕事にならないのでやるっきゃない。そこで @nishiuchikazuma さんに愚痴ると erlang になんかあるとの情報。どれどれ。

gen_fsm モジュール (erlang)

ググるとこんなのが出てきました。
Finite State MachinesってこれErlangのドキュメントのようです。そう、もともとは交換機用の言語だから状態マシンバリバリだったはずなんですよね。あって当たり前。この例も電話機の動作をFSMで表現しています。
ではと gen_fsmを読むと…
Module Summary: Deprecated and replaced by gen_statem
とあってもう古いですぅってことになってます。じゃあ、新しいのを見てみましょう。
にしても Summary に中身も書いてもらえないってちょっと gen_fsm が不憫ですね。Obsolete であることを間違いなく伝えたいのは分かりますので、中身はキチンと書いてリスペクトを示した上でやって欲しいところです。

gen_statem モジュール (erlang)

gen_fsmを追いやったのがgen_statemのようです。stream に空目してしまいますが statem です。ざっと見、Erlang の文法を知らなくても、例を見れば何をしてるかだいたい分かります。Elixir から Erlang モジュールは難なく使えるようですが、ちょっと今 Erlang まで手を広げたくないところです。Elixirワールドから出ないようにしておきたいです。

GenStateMachine モジュール (elixir)

とまあよろしくあるんですね、Elixir用のFSMモジュール GenStateMachine が。
statem のラッパのようです。これ先頭に簡単な例がありますので、ざっと読んでいくと何となく分かります。今すぐ使えそうです。

関数型言語に persistent data

例を見ると回数をカウントしているあたりで不穏な空気が流れます。先をどんどん読んでいくと、型の説明に
data()
The persistent data (similar to a GenServer’s state) for the GenStateMachine
と出てきて persistent data とかいいんかいと突っ込んでしまいます。「GenServerの状態と似ぃちゅう」とかあり、「はてどこかで聞いたことがあるぞ GenServer…」と思ったら、Elixir本のOTPの説明で出て来てました。プロセスに関係してくるんですかね。OTPはもそっと先でやろうと思うので、ここも手を広げたくなるのをぐっと我慢で、GenStateMachineを習得しましょう。

GenStateMachine の例をやってみる

以下、さらりと書きますが、死ぬほど時間がかかってます。

  • 週末でそれなりに疲れてるのにやってしまった
    • ついでにあまり(かなり)やりたくない仕事をナリイキ上受けてしまって、心が荒んでた
  • のか、ケアレスミスとか細かくばらまいてる
    • 肝心の GenStateMachine を use してない
    • それも use つかうところ require とか書いてる
    • state ってところを stage って間違える
  • ってな状態で動かすと
    • こじゃんとエラーメッセージが出てくる
    • 一見何が原因なのかさっぱり分からない
    • 原因が typo なのにエラー修正の長い旅に出る

ちゃんとしたコンディションでやらないといけませんね。でも、楽しくてやめられないっす。

GenStateMachine の例をやってみる

まずは GenStateMachine の例をそのまんまやってみます。そのまんまやるだけで凄まじい時間を食いました。

プロジェクトを作る

せっかくなんで mix に慣れます。
Elixir Schoolmix とElixir本を見ながらプロジェクトを作りましょう。

$ mix fsm
** (Mix) The task "fsm" could not be found

冒頭で拒否られるとつらいですね。付け焼き刃なんでこうなってしまいます。mix help してみましょう。正しくはこちら。

$ mix new fsm
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/fsm.ex
* creating test
* creating test/test_helper.exs
* creating test/fsm_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd fsm
    mix test

Run "mix help" for more commands.

はい、最後の行、さっき失敗したときに言ってほしかったですね。
つぎに、言われたとおり fsm ディレクトリに行って用もないのにテストしてみます。

$ mix test
Compiling 1 file (.ex)
Generated fsm app
..

Finished in 0.04 seconds
1 doctest, 1 test, 0 failures

Randomized with seed 703844

ふーん、です。次、行きます。mix.exs を触ります。

$ cat mix.exs
defmodule Fsm.MixProject do
  use Mix.Project

  def project do
    [
      app: :fsm,
      version: "0.1.0",
      elixir: "~> 1.7",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
    ]
  end
end

これに使うモジュールを記述して依存性を明示しておきます。

$ tail mix.exs

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:gen_state_machine, "~> 2.0.3"}
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
    ]
  end
end

と1行加えます。GenStateMachine への依存を書くのに gen_state_machine と書くというのは一体どこで知るんでしょうかね。GenStateMachine ドキュメントでは左上のタイトルに唯一出現してますが、それを見ないとならないのでしょうか。ちょっと分かりません。いずれ分かるだろうから次に行きます。

依存性を書いただけだと効力を発揮しないので、mix deps.get して依存性を追加します。

$ mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
New:
  gen_state_machine 2.0.3
* Getting gen_state_machine (Hex package)

mix は奥が深そうなので、このあたりはおまじないと思ってやりました。

GenStateMachine の例を突っ込む

lib ディレクトリに lib/fsm.ex が作られてるので、GenStateMachine の Example を、上の方の記述は放置して、ファイルの後の hello world の部分にそのまま入れていきます。

defmodule FSM do
  use GenStateMachine

  # Callbacks

  def handle_event(:cast, :flip, :off, data) do
    {:next_state, :on, data + 1}
  end

  def handle_event(:cast, :flip, :on, data) do
    {:next_state, :off, data}
  end

  def handle_event({:call, from}, :get_count, state, data) do
    {:next_state, state, data, [{:reply, from, data}]}
  end
end

あ、例ではモジュール名は Switch ですが、ここは FSM (Finite State Machine) としてます。(これの性で後でハマります。)
状態マシンはこのように記述されてます。

  • 状態は :on と :off の2つ
  • 状態遷移は :flip のみ
    • :off で :flip されると :on になる
    • :on で :flip されると :off になる
  • on になった回数を数えている
    • :get_count で回数が表示される

シーソー型のスイッチをパチパチして電灯を on/off するイメージです。
では、動かしてみます。

$ mix -S iex
** (Mix) Mix only recognizes the flags --help and --version.
You may have wanted to invoke a task instead, such as "mix run"

Usage: mix [task]

Examples:

    mix             - Invokes the default task (mix run) in a project
    mix new PATH    - Creates a new Elixir project at the given path
    mix help        - Lists all available tasks
    mix help TASK   - Prints documentation for a given task

The --help and --version flags can be given instead of a task for usage and versioning information.

うわ、派手に失敗しました。ヒントがたくさんあるのは良いですが、ドサッと出てくると驚きます。はい、mix コマンドじゃなくて、ここは iex コマンドでした。

$ iex -S mix
Erlang/OTP 21 [erts-10.0.8] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]

==> gen_state_machine
Compiling 3 files (.ex)
Generated gen_state_machine app
==> fsm
Compiling 1 file (.ex)
Generated fsm app
Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

いい感じです。では状態マシンを作りましょう。

iex(1)> {:ok, pid} = GenStateMachine.start_link(FSM, {:off, 0})
{:ok, #PID<0.169.0>}

ふーん、pid を返すと。このプロセスが状態マシンですな。では状態を遷移してみましょう。

{:ok, #PID<0.169.0>}
iex(2)> GenStateMachine.cast(pid, :flip)
:ok
iex(3)> GenStateMachine.cast(pid, :flip)
:ok
iex(4)> GenStateMachine.cast(pid, :flip)
:ok
iex(5)> GenStateMachine.cast(pid, :flip)
:ok

エラーは起こしませんが、何が起こってるのかわかりません。

iex(6)> GenStateMachine.call(pid, :get_count)
2
iex(7)> GenStateMachine.cast(pid, :flip)
:ok
iex(8)> GenStateMachine.call(pid, :get_count)
3
iex(9)> GenStateMachine.cast(pid, :flip)
:ok
iex(10)> GenStateMachine.call(pid, :get_count)
3
iex(11)> GenStateMachine.cast(pid, :flip)     
:ok
iex(12)> GenStateMachine.call(pid, :get_count)
4

2回に1度数字が増えるので、どうやら on/off を繰返しているのは間違いなさそうです。

状態表示を追加する

あんまりなんで、ちょっとだけ見えるようにしてみます。

  def handle_event(:cast, :flip, :off, data) do
    IO.puts(":off -> :on")
    {:next_state, :on, data + 1}
  end

  def handle_event(:cast, :flip, :on, data) do
    IO.puts(":on -> :off")
    {:next_state, :off, data}
  end

  def handle_event({:call, from}, :get_count, state, data) do
    {:next_state, state, data, [{:reply, from, data}]}
  end

これでパチパチすると on/off が繰り返されてるのが分かります。

iex(1)> {:ok, pid} = GenStateMachine.start_link(FSM, {:off, 0})
{:ok, #PID<0.178.0>}
iex(2)> GenStateMachine.cast(pid, :flip)
:off -> :on
:ok
iex(3)> GenStateMachine.cast(pid, :flip)
:on -> :off
:ok
iex(4)> GenStateMachine.cast(pid, :flip)
:off -> :on
:ok
iex(5)> GenStateMachine.cast(pid, :flip)
:on -> :off
:ok
iex(6)> GenStateMachine.call(pid, :get_count)
2

はげしいエラーメッセージ

最初に、今回は時間がかかっと書いてますが、ケアレスミスが多かったのに加えて、エラーメッセージが派手なんで原因がわかりにくかったということがあります。これ見てください。何が悪いかわかりますか?

iex(1)> {:ok, pid} = GenStateMachine.start_link(FSW, {:off, 0})   
** (MatchError) no match of right hand side value: {:error, :undef}
    (stdlib) erl_eval.erl:450: :erl_eval.expr/5
    (iex) lib/iex/evaluator.ex:249: IEx.Evaluator.handle_eval/5
    (iex) lib/iex/evaluator.ex:229: IEx.Evaluator.do_eval/3
    (iex) lib/iex/evaluator.ex:207: IEx.Evaluator.eval/3
    (iex) lib/iex/evaluator.ex:94: IEx.Evaluator.loop/1
    (iex) lib/iex/evaluator.ex:24: IEx.Evaluator.init/4
iex(1)> 
12:43:59.832 [error] GenStateMachine #PID<0.179.0> terminating
** (UndefinedFunctionError) function FSW.init/1 is undefined (module FSW is not available)
    FSW.init({:off, 0})
    (stdlib) gen_statem.erl:697: :gen_statem.init_it/6
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
State: {:undefined, :undefined}
Callback mode: :undefined
Postponed events: []
State: {:undefined, :undefined}
** (EXIT from #PID<0.177.0>) shell process exited with reason: an exception was raised:
    ** (UndefinedFunctionError) function FSW.init/1 is undefined (module FSW is not available)
        FSW.init({:off, 0})
        (stdlib) gen_statem.erl:697: :gen_statem.init_it/6
        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

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

よーく見れば (module FSW is not available) とあるので FSM と間違えて FSW と打ってるというのが分かります。が、それは結果を知っているからそこに目が行くのであって、
function FSW.init/1 is undefined
に目が行くと、「あれ? init 関数も定義しないとダメなの?」と、放り出されたわけでもないのに一人で大海原に飛び込んで「例だけじゃわからないなぁ。どれどれ GenStateMachine の init コールバック関数の説明を読むか」と、長い旅に出てしまいます。

はい、今回は長い旅を何度もして来ました。怪我しないと覚えないですが、やっぱりチトこの時間は辛い。

参考文献

GenStateModule
Mix, Elixir School(日本語版)
Elixir 1.7.3 (english)
Elixir(日本語版)

難参考文献

はじめてなElixir(3)
はじめてなElixir(7)

追記

もうひとつの例の方も同様に動きました。

defmodule Switch do
  use GenStateMachine, callback_mode: :state_functions

  def off(:cast, :flip, data) do
    {:next_state, :on, data + 1}
  end
  def off(event_type, event_content, data) do
    handle_event(event_type, event_content, data)
  end

  def on(:cast, :flip, data) do
    {:next_state, :off, data}
  end
  def on(event_type, event_content, data) do
    handle_event(event_type, event_content, data)
  end

  def handle_event({:call, from}, :get_count, data) do
    {:keep_state_and_data, [{:reply, from, data}]}
  end
end

今のところは、最初の記述方法の方が好みです。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2