How should I apply Sasa Juric's fsm in the right way?

Hello guys,

On my journey learning Elixir I encounterd a new problem (or challenge).

This the context of the problem. I have a program/function generating simple values like: :one, :one, :one, :two, :one, :two, …

After each generated value a function f(x) is directly called and the function value is an input for a state transition function. (Actually I’m calculating a fitness function in a machine learning program.)

For some reasons I needed a lightweight Finite State Machine. So I decided to use SasaJuric’s fsm. It’s using only macros and data structures instead of state servers.

The solution I thought of is; HbsFsm is the state engine:

case f(x) do
:one -> HbsFsm.one
:two -> HbsFsm.two
end

Following is the code I tried of a simple example using the fsm:

defmodule HbsFsm do

    use Fsm, initial_state: :my_state, initial_data: 0

    defstate my_state do
    
      defevent one , data: sum do
        next_state(:my_state, sum)
      end
      
      defevent two , data: sum do
        next_state(:my_state, sum + 1 )
      end 
      
    end
end

#----------------------------------------------------------

defmodule FsmTest do

 use ExUnit.Case

 test "Process event" do
 
   assert (
  
   x = :one
  
   HbsFsm.new
  
   case x do
    :one -> HbsFsm.one
    :two -> HbsFsm.two
   end )
  
 end
   
end

Running this code gives the next error message:

  1. test Process event (FsmTest)
    test/testfsm_test.exs:5
    ** (UndefinedFunctionError) function HbsFsm.one/0 is undefined or private. Did you mean one of:

        * one/1
    

    stacktrace:
    (hbsfsm) HbsFsm.one()
    test/testfsm_test.exs:14: (test)

My question is: How should I apply Sasa Juric’s fsm in the right way?

Thanks in advance,

Thiel

1 Like

If I head to guess you need to do

{:ok, pid} =  HbsFsm.new

HbsFsm.one(pid)

Basically, think of Hbs.Fsm as spawning an instance of an fsm that you need to pass around the pid of, vs spawning a global FSM.

As an aside, I’m not sure I’d recommend using this macro based API, and honestly I’m not sure if @sasajuric still recommends using them. I think that they can be a handy short hand when you want to get something going but for learning purposes I’m afraid it hides too much away.

There’s also the issue that it doesn’t teach you want the ordinary fsm syntax / usage looks like which means you aren’t well equipped to tackle even identical functionality in other code bases when you’re first starting out.

FSM is data based, not process based, so you need to take the result and pass it as the first argument to state functions:

fsm = HbsFsm.new
next_fsm = HbsHsm.one(fsm)
# ...

See example usage here.

When it comes to my FSM library, the machine is not powered by a separate process, and I still believe this is usually a more suitable approach to build FSMs compared to gen_statem (or gen_fsm). That said, I indeed don’t advise using my library anymore (see my comments in this issue). Plain pattern matching backed by maps or structs IMO works just fine.

2 Likes

The fsm lib is used in sigaws AWS V4 signature library for testing. Specifically for reading the AWS signature testsuite data file. Take a look at this file:

The fsm lib was used here before becoming aware of Sasa Juric not recommending it any longer! Didn’t put in any effort to change its usage as it is used only for testing and it just works!

Take a look at it if you are interested.

1 Like

Hi mr Sasa Juric,

I read the documentation of the FSM module and did the following test:

20 test “basic” do
21 assert(
22
23 fsm = BasicFsm.new
24 next_fsm = BasicFsm.run(fsm)
25
26 )

and it gave this error message:

** (SyntaxError) test/fsm_test.exs:24: syntax error before: next_fsm
(elixir) lib/code.ex:370: Code.require_file/2
(elixir) lib/kernel/parallel_require.ex:57: anonymous fn/2 in
Kernel.ParallelRequire.spawn_requires/5

I spent a lot of time to find the syntax error but unfortunately I did
n’t succeed.

Can you let me know what went wrong?

Many thanks in advance,

Thiel Chang

Ps. I am reading your excellent book and I love it. You are a true
professional and a very good Elixir teacher!

The assert(…) the not correct.

test "basic" do
    fsm = BasicFsm.new
    next_fsm = BasicFsm.run(fsm)
    assert :running == BasicFsm.state(next_fsm)
end
1 Like

Hi Benjamin,

Many thanks for your quick reaction! To test your suggestion I used a test case of the defmodule DataFsm of FSM. See the code:

 fsm         = DataFsm.new
 next_fsm = DataFsm.run(fsm, 15)
 next_fsm = DataFsm.slowdown(next_fsm, 10)
 next_fsm = DataFsm.slowdown(next_fsm, 1 )
 IO.inspect(next_fsm)
 ( assert :running == DataFsm.state(next_fsm) )

with test output: %FsmTest.DataFsm{data: 4, state: :running}, which shows it works.

Many thanks for taking your time to help me.

Thiel Chang