Papillon6814

Papillon6814

Does this look ok as a Finite State Machine?

Hello. I 'm creating a mix project which builds a finite state machine on redis. While I’m writing code of the repository, I referred a repository finist which is written in ruby.

Implementation

defmodule Dfm do
  @moduledoc """
  Documentation for `Dfm`.
  """

  require Logger

  @script """
  local curr = redis.call("GET", KEYS[1])
  local next = redis.call("HGET", KEYS[2], curr)
  if next then
    redis.call("SET", KEYS[1], next)
    return { next, true }
  else
    return { curr, false }
  end
  """

  @redis_host "localhost"
  @redis_port 6379

  defp conn() do
    with {:ok, conn} <- Redix.start_link(host: @redis_host, port: @redis_port) do
      conn
    else
      error -> raise "Failed to connect to #{@redis_host}:#{@redis_port} #{error}"
    end
  end

  def flushall() do
    conn = conn()
    Redix.command(conn, ["FLUSHALL"])

    Logger.info("Flushed all")
  end

  @doc """
  Initializes state of automaton.
  """
  @spec initialize(String.t(), integer(), String.t()) :: Redix.Protocol.redis_value()
  def initialize(key_name, db_index, initial_state) do
    conn = conn()
    name = name(key_name)

    Redix.command!(conn, ["SELECT", db_index])
    Redix.command!(conn, ["SET", name, initial_state, "NX"])
  end

  @spec name(String.t()) :: String.t()
  defp name(key_name), do: "finite:#{key_name}"

  @spec event_key(String.t(), String.t()) :: String.t()
  defp event_key(key_name, event), do: "#{key_name}:#{event}"

  @doc """
  Defines how automaton changes the state.
  """
  @spec on(String.t(), integer(), String.t(), String.t(), String.t()) :: Redix.Protocol.redis_value()
  def on(key_name, db_index, event, current_state, next_state) do
    conn = conn()

    Redix.command!(conn, ["SELECT", db_index])
    Redix.command!(conn, ["HSET", event_key(key_name, event), current_state, next_state])
  end

  @doc """
  Removes a pattern of state change.
  """
  @spec rm(String.t(), integer(), String.t()) :: Redix.Protocol.redis_value()
  def rm(key_name, db_index, event) do
    conn = conn()

    Redix.command!(conn, ["SELECT", db_index])
    Redix.command!(conn, ["HDEL", event_key(key_name, event)])
  end

  @doc """
  Return current state.
  """
  @spec state(String.t(), integer()) :: Redix.Protocol.redis_value()
  def state(key_name, db_index) do
    conn = conn()

    Redix.command!(conn, ["SELECT", db_index])
    Redix.command!(conn, ["GET", name(key_name)])
  end

  @spec send_event(String.t(), integer(), String.t()) :: Redix.Protocol.redis_value()
  defp send_event(key_name, db_index, event) do
    conn = conn()

    Redix.command!(conn, ["SELECT", db_index])
    Redix.command!(conn, ["EVAL", @script, 2, name(key_name), event_key(key_name, event)])
  end

  @doc """
  Triggers state change.
  """
  @spec trigger(String.t(), integer(), String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def trigger(key_name, db_index, event) do
    [state, result] = send_event(key_name, db_index, event)
    do_trigger(state, result)
  end

  defp do_trigger(state, nil), do: {:error, state}
  defp do_trigger(state, _), do: {:ok, state}
end

Test

defmodule DfmTest do
  use ExUnit.Case
  doctest Dfm

  describe "dfm" do
    test "simple automaton" do
      Dfm.flushall()

      1..5
      |> Enum.to_list()
      |> Enum.each(fn n ->
        key_name = "user#{n}"
        db_index = 15

        state1 = "a"
        state2 = "b"
        state3 = "c"
        trigger1 = "x"
        trigger2 = "y"
        invalid_trigger = "invalid"

        # NOTE: Define state changes
        Dfm.initialize(key_name, db_index, state1)
        Dfm.on(key_name, db_index, trigger1, state1, state2)
        Dfm.on(key_name, db_index, trigger1, state2, state3)
        Dfm.on(key_name, db_index, trigger1, state3, state1)
        Dfm.on(key_name, db_index, trigger2, state1, state3)

        assert Dfm.state(key_name, db_index) == state1
        assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger1)
        assert Dfm.state(key_name, db_index) == state2
        assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger1)
        assert Dfm.state(key_name, db_index) == state3
        assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger1)
        assert Dfm.state(key_name, db_index) == state1
        assert {:ok, state3} = Dfm.trigger(key_name, db_index, trigger2)
        assert Dfm.state(key_name, db_index) == state3

        # NOTE: Invalid patterns
        assert {:error, state3} = Dfm.trigger(key_name, db_index, trigger2)
        assert {:error, state3} = Dfm.trigger(key_name, db_index, invalid_trigger)
      end)
    end
  end
end

That’s all of this repository. I think it works as a finite state machine properly. But I’m not a skilled engineer, so I’d like to show the codes to everyone. I want advice for the repository!
Thank you.

Most Liked

karlosmid

karlosmid

Hi, based on principle do not invent your own wheel, you should implement your finite state machine using this simple library that will guarantee that your machine is a finite state machine:

https://github.com/subvisual/fsmx

Sebb

Sebb

Or dont use any lib at all.

def handle(:state_1, :signal_a) do
  ...
  :new_state
end
MrDoops

MrDoops

I’d recommend a vanilla elixir module and struct + functions for the state machine model. Then a GenServer (possibly dynamically supervised & registered) wrapping the FSM struct and persisting to a system of your choice (Redis, Postgres, etc).

Where Next?

Popular in Questions Top

New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
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
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
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

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement