View Source Statechart (Statechart v0.1.0)

A pure-Elixir implementation of statecharts inspired by

installation

Installation

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

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

concepts

Concepts

We'll model a simple traffic light to illustrate some statechart concepts.

traffic light diagram

  • This "machine" defaults to the off state (that's what the dot-arrow signifies).
  • If we then send the machine a TOGGLE event, it transitions to the on state. From there, it automatically drops into the red state (again, because of the dot-arrow). At this point, the machine is in both the on and red states.
  • If we send it a NEXT event, we transition to the green state (which you can also think of as the on/green state). Another NEXT event, and we transition to the yellow state. In this way, the light will just keep cycling through the colors.
  • If we send it a TOGGLE at this point, it will transition back to off.
  • If we now send the machine a NEXT event (while it's in the off state), nothing happens.

usage

Usage

There are three steps to modeling via the Statechart library:

We'll model the above traffic light using these three steps.

define

DEFINE

defmodule TrafficLight do
  use Statechart

  statechart default: :off do
    state :off do
      :TOGGLE >>> :on
    end

    state :on, default: :red do
      :TOGGLE >>> :off
      state :red,    do: :NEXT >>> :green
      state :yellow, do: :NEXT >>> :red
      state :green,  do: :NEXT >>> :yellow
    end
  end
end

instantiate

INSTANTIATE

The module containing your statechart definition automatically has a new/0 function injected into it.

traffic_light = TrafficLight.new()

It returns you a statechart/0 struct that you then pass to all the 'MANIPULATE' functions.

manipulate

MANIPULATE

The machine starts in the off state:

[:off] = Statechart.states(traffic_light)
true   = Statechart.in_state?(traffic_light, :off)
false  = Statechart.in_state?(traffic_light, :on)

Send it a NEXT event without it being on yet:

traffic_light = Statechart.trigger(traffic_light, :NEXT)
# Still off...
true = Statechart.in_state?(traffic_light, :off)
# ...but we can see that the last event wasn't valid:
:error = Statechart.last_event_status(traffic_light)

Let's turn it on:

traffic_light = Statechart.trigger(traffic_light, :TOGGLE)
[:on, :red]   = Statechart.states(traffic_light)
true  = Statechart.in_state?(traffic_light, :on)
true  = Statechart.in_state?(traffic_light, :red)
false = Statechart.in_state?(traffic_light, :off)
false = Statechart.in_state?(traffic_light, :green)

Now the NEXT events will have an effect:

traffic_light = Statechart.trigger(traffic_light, :NEXT)
[:on, :green] = Statechart.states(traffic_light)

error-checking

Error-checking

Statechart has robust compile-time checking. For example, compiling this module will result in a StatechartError at the state :on line.

defmodule ToggleStatechart do
  use Statechart

  statechart default: :on do
    # Whoops! We've misspelled "off":
    state :on, do: :TOGGLE >>> :of
    state :off, do: :TOGGLE >>> :on
  end
end

other-statechart-state-machine-libraries

Other statechart / state machine libraries

With a plethora of other related libraries, why did we need another one? I wanted one that had very strict compile-time checks and a simple DSL.

Other libraries you might look into:

Link to this section Summary

DEFINE

Register a transtion from an event and target state.

Create a statechart node.

Create and register a statechart to this module. May only be used once per module.

MANIPULATE

Determine if the given state is in the given compound state

Returns :ok is last event was valid and caused a transition

Get the current compound state

Send an event to the statechart

Link to this section Types

@type event() :: term()
@type state() :: term()
@opaque statechart()

Link to this section DEFINE

Link to this macro

event >>> target_state

View Source (macro)

Register a transtion from an event and target state.

statecharterror-raised-when

StatechartError raised when...

  • event is non-atom
  • event occurs elsewhere amongst this node's ancestors or descendents
  • target_state doesn't exist
  • >>>/2 is called outside of a state block
Link to this macro

state(name, opts \\ [], do_block)

View Source (macro)

Create a statechart node.

name must be an atom and must be unique amongst nodes defined in this module's statechart. The way to have multiple nodes sharing the same name is to define statechart partials in separate module and then insert those partials into a parent statechart.

statecharterror-raised-when

StatechartError raised when...

  • name is non-atom
  • name is non-unique (another node already has the same name)
  • assigning a default to a leaf node
  • a default targets a non-descendent
  • state/2 is called outside of a statechart block
Link to this macro

statechart(opts \\ [], do_block)

View Source (macro)

Create and register a statechart to this module. May only be used once per module.

defmodule ToggleStatechart do
  use Statechart

  statechart do
    state :on, default: true, do: :TOGGLE >>> :off
    state :off, do: :TOGGLE >>> :on
  end
end

statechart/2 accepts a :module option. In the below example, the module containing the statechart is Toggle.Statechart

defmodule Toggle do
  use Statechart

  statechart module: Statechart do
    state :on, default: true, do: :TOGGLE >>> :off
    state :off, do: :TOGGLE >>> :on
  end
end

In this way, many statecharts may be declared easily in one file:

defmodule MyApp.Statechart do
  use Statechart

  # module: MyApp.Statechart.Toggle
  statechart module: Toggle do
    state :on, default: true, do: :TOGGLE >>> :off
    state :off, do: :TOGGLE >>> :on
  end

  # module: MyApp.Statechart.Switch
  statechart module: Switch do
    state :on, default: true, do: :SWITCH_OFF >>> :off
    state :off, do: :SWITCH_ON >>> :on
  end
end

statecharterror-raised-when

StatechartError raised when...

Link to this section MANIPULATE

Link to this function

in_state?(statechart, state)

View Source
@spec in_state?(statechart(), state()) :: boolean()

Determine if the given state is in the given compound state

Link to this function

last_event_status(statechart)

View Source
@spec last_event_status(statechart()) :: :ok | :error

Returns :ok is last event was valid and caused a transition

@spec states(statechart()) :: [state()]

Get the current compound state

Link to this function

trigger(statechart, event)

View Source
@spec trigger(statechart(), event()) :: statechart()

Send an event to the statechart