Gearbox - A functional state machine with an easy-to-use API, inspired by both Fsm and Machinery

Hey guys! :wave: Just published my first Hex package, hoping to gain some feedback here!

Gearbox is a functional state machine with an easy-to-use API, inspired by both Fsm and Machinery, that inspiration mostly stemmed from two things:

  • I liked Fsm’s functional approach without being backed by a process, but I didn’t really like the DSL nature of it.
  • I really liked Machinery’s API, which is very close to what I want, however, I decided against it when I couldn’t pull the package for a Phoenix 1.4 project because it has a dependency lock on Phoenix 1.3. I also did not like that it was backed by a single GenServer process.

I looked at both of them and thought I could use this opportunity to create something that I want, so here be Gearbox!

I wrote out a longer description and rationale behind the project, along with some deliberate decisions taken (e.g: no events and no callbacks in hopes of nudging users towards keeping them in domain contexts)

You can read more over at the GitHub link below and also on HexDoc!

Gearbox focuses on doing one thing and doing one thing right – state transitions. No surprise callbacks, no process management overhead, just pure functional state transitions.

Would love to hear your feedback!

6 Likes

I loved FSM’s style, but one question about this, how do you carry state-specific information without making it global on the struct?

1 Like

Sorry I’m not quite sure what you mean by state-specific information. Could you clarify what you mean by that? Perhaps an example would help!

Like for example a REGEX is a FSM, a state of the FSM holds the data of what that specific state parsed (if anything), each can hold different types of data.

Hey, would you mind to give some typical use cases for this library? Point me to some resources about Fsm/state transitions system? I’m not familiar to these.

I’m not 100% sure if I’m getting your question still, but here goes.

Gearbox does not currently ship with a data concept, which means it can’t really perfectly do what you want. But that’s also sort of the point of it (I’m open to suggestions/counter-argument). If you want more complex data in a state machine, you can build your own abstraction over it. The only thing that Gearbox wants to do is to help you transition from a state to another.

For the fun of it I did a quick hack to show what a contrived example of Regex matcher implementation would look like, the idea of this is to show that Gearbox does a very simple thing, and if you need complex data modelling you can abstract over it. (Like I said, still open to debate for this)


defmodule RegexMachine do
  use Gearbox,
    states: ~w(a b c),
    transitions: %{
      "a" => ["a", "b"],
      "b" => ["b", "c"]
    }
end

defmodule CustomRegex do
  def match(string) do
    run(string)
  end

  def run(string) when is_binary(string) do
    string
    |> String.split("", trim: true)
    |> run()
  end

  def run([_head | []]) do
    :match
  end

  def run([current | [next | _] = another] = chars) when is_list(chars) do
    map = %{state: current} # this is a bit of a hack because Gearbox currently expects a map

    case Gearbox.transition(map, RegexMachine, next) do
      {:ok, state} ->
        run(another)
      {:error, reason} ->
        :failed
    end
  end
end

CustomRegex.match("aaabbbc")

You can see how in that contrived example, all Gearbox does is to help you check the next state, the idea of data is provided by CustomRegex itself.

Note: It’s probably worth noting that Gearbox mainly stemmed from ecommerce-style applications where we see random state transitions that should never happen (a transaction going from pending -> refunded for example).

I hope that helps answers your question, if not then definitely ask away!

1 Like

Hey! Let me start off by giving you some context why I built Gearbox.

I work in an ecommerce company where we have a domain model called Transaction, but we sometimes see invalid state transitions for our transactions when it shouldn’t happen. For example, we saw a transaction goes from success to rejected, which isn’t a possible transition in our domain - if it’s marked as success, you should only be able to refund that transaction, not reject it.

We digged in a little and figured that this is happening due to some failure in some expire worker.

In that domain, we first create a transaction with pending_payment status, we then prompt user for payment and await the callback from payment provider. However at the same time, we queue a worker to timeout this payment in X minutes, if payment hasn’t been made.

In this particular scenario, the callback comes in, we mark the transaction as success, which is all good and expected for user, however our timeout worker continued to run after X minutes and it changed the transaction from success to timeout, a bug in the system.

We could of course update our worker to handle that logic to first check for that status of transaction, but that’s a lot of defensive programming, and what about when we add more states? The number of possible transitions grows exponentially, and if we were to defensively check against every single possible states, we’d have so many different cases to check for, that’s a code smell.

This is where something like a state machine comes in, the main idea of a state machine is really to eliminate impossible state transition. For example, let’s see how Gearbox solves this problem for us.

With Gearbox, you have to first tell your machine, what possible states can happen in your system:

defmodule Machine do
  use Gearbox,
    states: ~w(one two three four five) # finite states
end

We then have to very explicitly declare the possible transitions in the machine.

defmodule Machine do
    use Gearbox,
      states: ~w(pending_payment success timeout refunded) # finite states
+     transitions: %{
+       "pending_payment" => ~w(success timeout),
+       "success" => ~w(refunded),
+     }
end

Now when I look at the machine, I know:

  • exactly what can happen between two states
  • exactly what possible states can transition from pending
  • what are my final/acceptable states (states that can’t transition to something else)
  • I can be sure that my transaction will never be able to make illegal state transitions (pending_payment => refunded is not valid)

Does that sort of make sense to you? @thojanssens1 If not feel free to ask more questions!

Here’s some resources, but there are a lot of examples out in the wild, you can just Google for Finite State Machine :slight_smile:

There are honestly a lot of studies into FSM as well, and frankly I am not even well-versed in them all - I just needed to solve my own problem and thus born Gearbox :slight_smile:

6 Likes