Lab42.StateMachine v0.1.0 Lab42.StateMachine View Source
Lab42.StateMachine
Synopsis
A simple State Machine operating on a list of input values, a map of transition definitions and an accumulator.
What is the Transition Definitions Map?
It maps each state
to a list of transitions
. Each transition
is of the following format, as
defined by type transition_t
{trigger, transformer, updater, new_state}
State, did you say state?
Yes I did, and what I mean by it is that the State Machine keeps track of its state (very surprising) and some data, passed into by the user.
How do these Transitions transform the input?
The State Machine processes its input in a loop in which, depending on the current input element, designated
as input
and the current state, designated as current_state
a transition is triggered.
Data is passed in like an accumulator in Enum.reduce
it is designated as data
.
The triggered transition will define the current_state
of the next loop, if any, and perform actions defining what
goes to the output and how data
changes.
As one might guess these actions are performed by the transformer
and the updater
of the triggered transition.
So what is the contract?
Starting the whole thing.
Create the Transition Defintions Map, knowing that the State Machine will start with current_state
equal to :start
,
yes I know, naming is difficult ;).
Then run the whole thing like that:
Lab42.StateMachine.run(input, my_data, %{})
The empty map passed in will cause havoc though
iex(0)> input = ~w(alpha)
...(0)> my_data = %{n: 42}
...(0)> Lab42.StateMachine.run(input, my_data, %{})
{:error, "No transitions found for current state", :start}
Specifying Transitions
A minimal example might be a line counter
iex(1)> input = ~w{alpha beta}
...(1)> count = 0
...(1)> states = %{
...(1)> start: [ {true, fn {_, line} -> line end, fn count, _ -> count + 1 end, :start} ]
...(1)> }
...(1)> run(input, count, states)
{:start, ~w{alpha beta}, 2}
N.B. That the true
trigger always matches, other triggers are either regular expressions, which are used
as follows: Regex.match(trigger, input)
, or functions, which are used as one might easily guess, thusly: trigger.(input)
One could argue that a default behavior of copying the current input to the output might be convenient, but that might
lead to difficulties in debugging state machines. (Maybe in later versions with an option for run
?)
On the same token some StateMachines, like the counter above collect the output without needing it, although we will learn below how to avoid this, a global option to not collect will make the Transitions Map more concise.
Therefore the following will happen
iex(2)> input = ~w{alpha beta}
...(2)> states = %{
...(2)> start: []
...(2)> }
...(2)> run(input, nil, states)
{:error, "No trigger matched the current input \"alpha\"", :start}
Let us return to the correctly working example, let us simplify that rather expressive transition
iex(3)> input = ~w{alpha beta}
...(3)> states = %{
...(3)> start: [ {true, :id, fn count, _ -> count + 1 end, :start} ]
...(3)> }
...(3)> run(input, 0, states)
{:start, ~w(alpha beta), 2}
So we can use a shortcut for copying the input to the output, that is better already, but still, why create the output that is not needed, let us use the atom form of the transformer function
iex(4)> input = ~w{alpha beta}
...(4)> states = %{
...(4)> start: [ {true, fn _ -> :ignore end, fn count, _ -> count + 1 end, :start} ]
...(4)> }
...(4)> run(input, 0, states)
{:start, ~w(), 2}
As there is a shortcut for :id
so there is one for :ignore
iex(5)> input = ~w{alpha beta}
...(5)> states = %{
...(5)> start: [ {true, :ignore, fn count, _ -> count + 1 end, :start} ]
...(5)> }
...(5)> run(input, 0, states)
{:start, ~w(), 2}
But what if we want to have :ignore
in the output? Let us assume that we want to replace all "alphas"
with
:ignore
. We can use the tuple form of the transformer in this case.
This example also demonstrates that we do not need to specify the updater and the new state if these do
not change and that even pushing the input to the output can be omitted, therefore the transitions {true, :id}
and
{true}
have the same semantics.
And a third simplifaction is that we can omit to pass nil as data, but note that it will present in the result.
iex(6)> input = ~w{alpha beta alpha}
...(6)> states = %{
...(6)> start: [
...(6)> {~r{alpha}, fn _ -> {:push, :ignore} end},
...(6)> {true}] }
...(6)> run(input, states)
{:start, [:ignore, "beta", :ignore], nil}
Now let us look at a complex example, that will use the following features, not yet explored:
- Stopping with the
:halt
atom form of the transformer, by its shortcut - Stopping with the
{:halt, value}
tuple form of the transformer - Creating a function to rerun with the same states
- Using constant functions
Using function triggers
iex(7)> summer = fn data, {_, input} -> %{data|sum: data.sum + input} end ...(7)> states = %{ ...(7)> start: [ ...(7)> {&(&1>8), :halt}, ...(7)> {&(&1<1), fn {_, input} -> {:halt, input} end, constant(:negative)}, ...(7)> {&(rem(&1,2)==0), push_constant(:even), summer, :even}, ...(7)> {true} ], ...(7)> even: [ ...(7)> {&(rem(&1,2)==0), :halt, constant(:more_evens)}, ...(7)> {true, :id, summer } ...(7)> ] } ...(7)> state_machine = make_machine(%{sum: 0}, states) ...(7)> [ ...(7)> state_machine.(1..10|>Enum.into([])), ...(7)> state_machine.([1, 2, 3]), ...(7)> state_machine.([1, 9]), ...(7)> state_machine.([1, -1]), ...(7)> state_machine.([1, 3, 5]) ] [ {:even, [1, :even, 3], :more_evens}, {:even, [1, :even, 3], %{sum: 5}}, {:start, [1], %{sum: 0}}, {:start, [1, -1], :negative}, {:start, [1, 3, 5], %{sum: 0}} ]
Summary of Shortcuts and Transformer Semantics
As we have seen in the examples above, the StateMachine is driven by the Transitions Map and the result of the
transformer
function.
The returned data
value is influenced by the result of the updater
function.
In order to write shorter Transition Maps transitions can be shortened and symbolic function names can be used.
Let us see how transitions are normalized by the StateMachine before they are executed.
Specified | Normalize | Remarks |
---|---|---|
{trigger, function, function, state} | same value | already normalized |
{trigger, function, function} | {trigger, function, function, current_state} | normalization is done during tuntime, thusly current_state is known |
{trigger, function } | {trigger, function, identity2, current_state} | given the interface of updater identity2 is defined as fn d, _ -> d end |
|{trigger}
| {trigger, identity1, identity2, current_state}
| identity1
is for the transformer
and thusly fn {_, v} -> v end
As can be seen sometimes the defaults, are not available, because we need to provide the new_state
in the transition.
In that case the following symbols can be used for the transformer
Symbolic Transformer | Expanded to |
---|---|
:id | fn {_, v} -> v end |
:ignore | fn _ -> :ignore end |
:halt | fn _ -> :halt end |
For the updater
there is only :id
which expands to fn d, _ -> d end
as mentioned above.
In action that would give
iex(8)> states = %{
...(8)> start: [{true, :id, :id, :stop}],
...(8)> stop: [{true, fn _ -> {:halt, "..."} end, :id}]
...(8)> }
...(8)> run(~w[Headline Rest MoreRest], states)
{:stop, ~w[Headline ...], nil}
Link to this section Summary
Functions
A helper creating an updater function that sets data to a constant value
A convenience helper to run the same state machine on different inputs
A helper creating a transformer function that pushes a constant value to the output
Link to this section Types
incomplete_transition_t()
View Sourceincomplete_transition_t() :: {trigger_t()} | {trigger_t(), symbolic_transformer_t()} | {trigger_t(), symbolic_transformer_t(), symbolic_updater_t()} | {trigger_t(), symbolic_transformer_t(), symbolic_updater_t(), state_t()}
normalized_transitions_t()
View Sourcenormalized_transitions_t() :: [ transition_t() | incomplete_transition_t() | empty_t() ]
symbolic_transformer_t()
View Sourcesymbolic_transformer_t() :: transformer_t() | atom()
transition_t()
View Sourcetransition_t() :: {trigger_t(), transformer_t(), updater_t(), state_t()}
Link to this section Functions
constant(constant_value)
View Sourceconstant(const_type) :: const_type when const_type: any()
A helper creating an updater function that sets data to a constant value
A convenience helper to run the same state machine on different inputs
A helper creating a transformer function that pushes a constant value to the output