View Source Statechart (Statechart v0.1.0)
A pure-Elixir implementation of statecharts inspired by
- David Harel's Statecharts: a visual formalism for complex systems paper
- David Khourshid's JavaScript XState library
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.
- 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 theon
state. From there, it automatically drops into thered
state (again, because of the dot-arrow). At this point, the machine is in both theon
andred
states. - If we send it a
NEXT
event, we transition to thegreen
state (which you can also think of as theon/green
state). AnotherNEXT
event, and we transition to theyellow
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 tooff
. - If we now send the machine a
NEXT
event (while it's in theoff
state), nothing happens.
usage
Usage
There are three steps to modeling via the Statechart
library:
- DEFINE
- INSTANTIATE
MyStatechart.new/0
- MANIPULATE
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:
Machinery
as_fsm
gen_statem
- https://github.com/sasa1977/fsm
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
Link to this section DEFINE
Register a transtion from an event and target state.
statecharterror-raised-when
StatechartError
raised when...
event
is non-atomevent
occurs elsewhere amongst this node's ancestors or descendentstarget_state
doesn't exist>>>/2
is called outside of astate
block
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-atomname
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 astatechart
block
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...
statechart/2
is used more than once per module
Link to this section MANIPULATE
@spec in_state?(statechart(), state()) :: boolean()
Determine if the given state is in the given compound state
@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
@spec trigger(statechart(), event()) :: statechart()
Send an event to the statechart