mudasobwa

mudasobwa

Creator of Cure

Finitomata - FSM boilerplate based on callbacks

Finitomata provides a boilerplate for FSM implementation, allowing to concentrate on the business logic rather than on the process management and transitions/events consistency tweaking.

It reads a description of the FSM from a string in PlantUML format.

It validates the FSM is consistent, namely it has a single initial state, one or more final states, and no orphan states. If everything is OK, it generates a GenServer that could be used both alone, and with provided supervision tree. This GenServer requires to implement three callbacks

  • on_transition/4 — mandatory
  • on_failure/3 — optional
  • on_terminate/1 — optional

All the callbacks do have a default implementation, that would perfectly handle transitions having a single to state and not requiring any additional business logic attached.


Example:

defmodule MyFSM do
  @fsm """
  [*] --> s1 : to_s1
  s1 --> s2 : to_s2
  s1 --> s3 : to_s3
  s2 --> [*] : ok
  s3 --> [*] : ok
  """

  use Finitomata, @fsm

  def on_transition(:s1, :to_s2, event_payload, state_payload),
    do: {:ok, :s2, state_payload}
end

Most Liked

mudasobwa

mudasobwa

Creator of Cure

Package published to finitomata | Hex
(25ec5990045d025fba2bb1c59a92e08fc8a045fdd347fcdcb885f22c75a1f2d2)

Introduces Infinitomata module as a drop-in replacement of Finitomata.{start_fsm/4,transition/4,state/3}. It transparently runs in a cluster, leveraging process groups :pg to keep track of spawned instances.

Example from tests:

defmodule InfinitomataTest do
  use ExUnit.Case, async: true
  @moduletag :distributed

  setup do
    {_peers, _nodes} = Enfiladex.start_peers(3) # start 3 peers
    Enfiladex.block_call_everywhere(Infinitomata, :start_link, [])
  end

  test "many instances (distributed)" do
    for i <- 1..10 do
      Infinitomata.start_fsm("FSM_#{i}", Finitomata.Test.Log, %{instance: i})
    end

    assert Infinitomata.count(Infinitomata) == 10

    for i <- 1..10 do
      Infinitomata.transition("FSM_#{i}", :accept)
    end

    assert %{"FSM_1" => %{}} = Infinitomata.all(Infinitomata)

    for i <- 1..10 do
      Infinitomata.transition("FSM_#{i}", :__end__)
    end

    Process.sleep(1_000)

    assert Infinitomata.count(Infinitomata) == 0 # all finished ending state
    assert Infinitomata.all(Infinitomata) == %{}
  end
end
mudasobwa

mudasobwa

Creator of Cure

Why is that?

could you demo a door

Sure. The code below is untested, but the idea is correct.

defmodule Door do
  @fsm """
    unplugged --> |on!| closed
    closed --> |button| input
    closed --> |off| dismantled
    input --> |button| input,unlocked,locked
    unlocked --> |plumber| closed
    locked --> |plumber| closed
  """

  use Finitomata, fsm: @fsm, timer: 10, impl_for: :all, auto_terminate: true

  @impl true
  # entering `input`
  def on_transition(:closed, :button, button, %{code: code, entered: []} = state),
    do: {:ok, :input, Map.update(state, :entered, & [button | &1])}

  # processing input
  def on_transition(:input, :button, button, %{code: code, entered: current} = state) do
    current = [button | current]
    cond do
      valid?(current, code) -> {:ok, :unlocked, Map.put(state, entered: [])}
      length(current) >= 10 -> {:ok, :locked, Map.put(state, entered: [])}
      true -> {:ok, :input, Map.put(state, entered: current)}
    end
  end
 
  def on_timer(:locked, %{locked_from: timestamp} = state) do
    if elapsed_lock?(timestamp),
      do: {:transition, :plumber, state}, # unlock
      else: {:ok, state}
  end

  def on_timer(:input, state) do
    if elapsed_timeout?(timestamp),
      do: {:transition, :plumber, Map.put(state, :entered, [])}, # reset
      else: {:ok, state}
  end
end

I didn’t implement private helpers like elapsed?/1 and valid?/2 because they are trivial.

mmmries

mmmries

Thanks @mudasobwa your breakdown definitely improved my thinking. I can see how a generator might be a pain to deal with in source control etc. And the distinction between run-time, compile-time and deploy-time is helpful as well. Thanks again for the library, I’ll give it a run

Where Next?

Popular in Announcing Top

wmnnd
Hi there, for my project DBLSQD, I needed a file storage solution that is a bit more flexible than Arc. Because I thought others might f...
New
mathieuprog
Hello :waving_hand: Allow me to introduce you to Tz, an alternative time zone database support to Tzdata. Why another library? First a...
New
martinthenth
Hello everybody :wave: Recently, some of my colleagues talked about database ids and uuids and their problems, and I remembered the pain...
New
restlessronin
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub. Docs are at OpenaiEx User Gu...
152 10342 134
New
mikehostetler
I’m excited to announce Jido, a framework providing foundational primitives for building autonomous agent systems in Elixir. While develo...
New
bryanjos
Hi, I just published version 0.23.0 of Elixirscript. https://github.com/bryanjos/elixirscript/blob/master/CHANGELOG.md Most of the chan...
New
treble37
Just looking for a little feedback on a tiny helper library I built - Sometimes I find the need to convert maps with atom keys to maps w...
New
sabiwara
Dune is a sandbox for Elixir and aims to safely evaluate user-provided code. You can try it out using this basic Elixir playground made ...
New
hpopp
After just over two years in development, this latest version of Pigeon is what I finally consider done in regards to my original vision ...
New
Hal9000
Here is my first stab at this. README pasted below. https://github.com/Hal9000/elixir_random Comments and critiques are welcome. Thank...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 127089 1222
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement