StateMachine

Build Status

The goal of this work is to simplify building finite state machines in Elixir. Formal definition of finite state machines can be found on Wikipedia.

Here's an example of simple state machine created with this package:

defmodule Cat do
  use StateMachine

  defstruct [:name, :state, hungry: true]

  defmachine field: :state do
    state :asleep
    state :awake
    state :playing
    state :eating, after_enter: &Cat.feed_up/1

    event :wake do
      transition from: :asleep, to: :awake
    end

    event :give_a_mouse do
      transition from: :awake, to: :playing, unless: &Cat.hungry/1
      transition from: :awake, to: :eating, if: &Cat.hungry/1
      transition from: :playing, to: :eating
    end

    event :pet do
      transition from: [:eating, :awake], to: :playing
    end

    event :sing_a_lullaby do
      transition from: :awake, to: :asleep
      transition from: :playing, to: :asleep
    end
  end

  def hungry(cat) do
    cat.hungry
  end

  def feed_up(cat) do
    {:ok, %{cat | hungry: false}}
  end
end

And later use it like this:

cat = %Cat{name: "Thomas", state: :asleep}

# After event we get a context that contains modified (possibly) model in `model` field.
context = Cat.trigger(cat, :wake)
context.status      # => :done
context.old_state   # => :asleep
context.new_state   # => :awake
context.model.state # => :awake

# To learn about all/available states/events, use introspection:
# TODO...

Overview

If you're familiar with state machines in general, you can skip the rest of this readme.

States

States are named conditions which state machine can inhabit. State machine can be only in one state simultaneously. State is changed in response to some external event. States in our model are represented by atoms.

Events

Events are external messages that might cause state machine to change state. Event is a container for transitions.

Transitions

Transitions are pairs of states: source (from) and destination (to). The definition allows multiple source states for brevity, but destination is always deterministic. However it is possible to define an event that might move state machine from one state to either of multiple states based on exection of a guard (see below).

Callbacks

Callbacks are functions that can be called in various times during lifecycle. There are callbacks for states, events and transitions:

Callbacks can be of arity 0, 1 or 2:

Important to notice that callbacks cannot be defined inline as lambdas, because lambdas won't survive macro expansion.

Guards

Guards are functions that help to decide wether state machine can proceed with transition. In one event attempt it might run various guards whose transitions match formal signature, for this reason guards are not allowed to have side-effects. Guards are ran very first, before any callback. They basically just help to choose appropriate transition. For example, checking the balance before unlocking a bycicle in bikeshare system. For convenience both if and unless keywords are supported: respectively guard in if must return true to proceed and vice versa for unless. It's impossible to use lambdas here as well.

Validation

State machine definition is validated automatically on compile time. It will check if states used in transitions are declared first. It also attempts to catch non-determinism, i.e. if one event has two transitions from one state without guards.

Context

FSM as a structure

Basic mode of operation is in some sense static. State machine definition generates some functions and attributes on the module of your choice and then you just run then in whatever environment you want. This way provides greatest level of control.

FSM as a process

Second mode is dynamic in a way that structure that can behave as state machine becomes a separate process that accepts events as messages. This approach naturally maintains consistency of state cause concurrent events are nicely separated by process's mailbox. And it's totally feasible since processes in Elixir are cheap. Our goal is to use existing DSL to generate gen_statem-compatible definition. In other words it's a work in progress.

Installation

The package can be installed by adding state_machine to your list of dependencies in mix.exs:

def deps do
  [
    {:state_machine, "~> 0.1.0"}
  ]
end

The documentation can be found at https://hexdocs.pm/state_machine.