GenFSM - An Elixir wrapper for the :gen_fsm behaviour

I’ve been working on an Elixir wrapper for OTP’s :gen_fsm behaviour with Paul Hieromnimon. It doesn’t do much, besides making it a bit easier to use :gen_fsm's in your Elixir code.

It would be hugely appreciated if someone would go through the specs and documentation to assure that it is correct. Issues and pull requests are love. Also, it would be appreciated if issues were raised if something isn’t clear or is downright confusing.

N.B.: We do know of the work being done on :gen_statem, and we will wrap that when it is final and released in OTP.

5 Likes

Hi,

Thanks for this wrapper for :gen_fsm. I’m playing around with it by reimplementing the example of a code lock from the Erlang docs (http://erlang.org/doc/design_principles/fsm.html) but I can’t get the timeout to work.

No matter if I set a timeout in the result:

{:next_state, :open, {[], code}, 30000}

or I manually set a timeout using GenFSM.start_timer/2:

GenFSM.start_timer(30000, :timeout) {:next_state, :open, {[], code}}

the open(:timeout, state_data) event is never called. Am I doing something wrong or is this a bug in the spec?

3 Likes

Please open a bug on https://github.com/pavlos/gen_fsm/issues and I will have a look at it later :slight_smile:

1 Like

Are you sure nothing is happening while waiting for the timeout after returning a timeout value?

If you use GenFSM.start_timer/2 then that should result in a {:timeout, ref, msg} event being called, not a :timeout event. GenFSM.send_event_after/2 will result in what ever event you specify being called.

Robert

1 Like

I’ve ported the example to Elixir. It seems to work fine :slight_smile:

defmodule CodeLock do
  use GenFSM

  def start_link(code) do
    GenFSM.start_link(__MODULE__, Enum.reverse(code))
  end

  def button(pid, digit) do
    IO.puts "beeep!"
    GenFSM.send_event(pid, {:button, digit})
  end

  def init(code) do
    {:ok, :locked, {[], code}}
  end

  def locked({:button, digit}, {so_far, code}) do
    IO.inspect {:state, so_far, code}
    case [digit | so_far] do
      ^code ->
        # do_unlock(...)
        IO.puts "unlocked!"
        {:next_state, :open, {[], code}, 1000}

      incomplete when length(incomplete) < length(code) ->
        IO.puts "awaiting more digits"
        {:next_state, :locked, {incomplete, code}}

      _wrong ->
        IO.puts "wrong!"
        {:next_state, :locked, {[], code}}
    end
  end

  def open(:timeout, state) do
    # do_lock(...)
    IO.puts "SLAM! locked!"
    {:next_state, :locked, state}
  end
end

defmodule CodeLockTest do
  use ExUnit.Case

  test "punch in those numbers and await the timeout" do
    {:ok, pid} = CodeLock.start_link([1,2,3,4])

    CodeLock.button(pid, 1)
    CodeLock.button(pid, 2)
    CodeLock.button(pid, 3)
    CodeLock.button(pid, 4)
    # we should be open by now

    # wait for the timer...
    :timer.sleep 1100
  end
end

The output should look like this

beeep!
beeep!
beeep!
beeep!
{:state, [], [4, 3, 2, 1]}
awaiting more digits
{:state, [1], [4, 3, 2, 1]}
awaiting more digits
{:state, [2, 1], [4, 3, 2, 1]}
awaiting more digits
{:state, [3, 2, 1], [4, 3, 2, 1]}
unlocked!
* * * wait some time * * *
SLAM! locked!
2 Likes

Hi gausby and rvirding, you’re absolutely right. Embarrassingly enough I wasn’t careful when I specified the timeout. I thought I had to wait 3 seconds but with one more 0 then intended, the timeout was set for 30 seconds. Sorry about that. Clearly I coulnd’t wait 30 seconds but instead had you all up in arms :blush: On a happy note, so far I have found no issues with the spec and I’m thinking of using this wrapper in an upcoming project.

2 Likes

Great, and no worries! It was good to port and test the implementation with timeouts. I’ve made some PR’s to Pavlos and hopefully they will get merged soon…one of them contains the lock code example.

2 Likes

Hi,

Could it be that there’s a problem with the spec for synchronously executed finite state machines? I’ve just ported my code to use GenFSM.sync_send_event/2 rather than GenFSM.send_event/2. At runtime the VM complains that:

[error] ** State machine #PID<0.376.0> terminating
...
** Reason for termination = 
** {:"function not exported",
...

Kind regards

I’ve answered your question in the GitHub issues. It does support sync_send_event/2, as demonstrated in the turnstile example.

If you are into FSMs (and why wouldn’t you?) you should check out :gen_state_machine that wraps the new state machine introduced in Erlang 19. Overall I find it better than :gen_fsm as it allows you to postpone messages to the next state and easily do state transitions.

1 Like