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

sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 42920 311
New
tmbb
PhoenixWS - Websockets over Phoenix Channels Source code on Github here: GitHub - tmbb/phoenix_ws: Websockets implemented over Phoenix Ch...
New
josevalim
Hi everyone, We would like to announce that Plataformatec is working on a new MySQL driver called MyXQL. Our goal is to eventually integ...
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 10167 134
New
Crowdhailer
I have been updating a library that allows you to pipe between functions that use the erlang result tuple convention. Assuming you have ...
New
mbuhot
Leverage Open Api 3.0 (Swagger) to document, test, validate and explore your Plug and Phoenix APIs. Generate and serve a JSON Open API ...
New
wojtekmach
Hey everyone! Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New
mindok
What is ContEx? A pure Elixir server-side data plotting/charting library outputting SVG. It has nice barcharts in particular and works g...
New
Azolo
Hey everyone, I just released WebSockex which is a Elixir WebSocket client. WebSockex strives to work as a OTP special process, be RFC6...
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

Other popular topics Top

New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
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
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New

We're in Beta

About us Mission Statement