edisonywh

edisonywh

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

Hey guys! :waving_hand: 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!

https://github.com/edisonywh/gearbox

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!

Most Liked

edisonywh

edisonywh

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:

OvermindDL1

OvermindDL1

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

edisonywh

edisonywh

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 pendingrefunded for example).

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

Where Next?

Popular in Announcing Top

dominicletz
Hi, I thought I had posted my library before but seems I hadn’t. The project is still in early stages but it’s growing and so I think it...
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
mtrudel
Bandit is an HTTP server for Plug and WebSock apps. Bandit is written entirely in Elixir and is built atop Thousand Island. It can serve...
New
Jskalc
Hi! Today, after a couple weeks of development I’ve released v0.1 of LiveVue. It’s a seamless integration of Vue and Phoenix LiveView, i...
New
josevalim
Hello everyone, We have just released NimbleCSV which is a small and fast CSV parsing library for Elixir. It allows developers to define...
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
marcuslankenau
I feel kind of stuck with the absence of a proper xml library for Elixir. Currently I use SweetXML which was ok for me more or less to pa...
New
anshuman23
Hello all, I have been working on my proposed project called Tensorflex as part of Google Summer of Code 2018.. Tensorflex can be used f...
New
devonestes
Introducing assertions, the library that helps you write really great test assertions! GitHub: GitHub - devonestes/assertions: Helpful a...
New
New

Other popular topics Top

baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
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
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New

We're in Beta

About us Mission Statement