# lab42_simple_state_machine
<!--
DO NOT EDIT THIS FILE
It has been generated from the template `README.md.eex` by Extractly (https://github.com/RobertDober/extractly.git)
and any changes you make in this file will most likely be lost
-->
[![Build Status](https://travis-ci.org/RobertDober/lab42_simple_state_machine.svg?branch=master)](https://travis-ci.org/RobertDober/lab42_simple_state_machine)
[![Coverage Status](https://coveralls.io/repos/github/RobertDober/lab42_simple_state_machine/badge.svg?branch=master)](https://coveralls.io/github/RobertDober/lab42_simple_state_machine?branch=master)
[![Hex.pm](https://img.shields.io/hexpm/v/lab42_simple_state_machine.svg)](https://hex.pm/packages/lab42_simple_state_machine)
[![Hex.pm](https://img.shields.io/hexpm/dw/lab42_simple_state_machine.svg)](https://hex.pm/packages/lab42_simple_state_machine)
[![Hex.pm](https://img.shields.io/hexpm/dt/lab42_simple_state_machine.svg)](https://hex.pm/packages/lab42_simple_state_machine)
## A simple state machine.
`SimpleStateMachine` is a minimalistic approach to write _State Machines_ which operate on a list of inputs.
The machine is defined by a map, mapping, transitions to each state, called `transition_map`.
%{ start: [ transition, ... ],
some_state: [transition, ...] }
### Format of a Transition
`{trigger, transition_fn, new_state}` or, if the state is not supposed to change, `{trigger, transition_fn}`
For each input in the list of inputs the machine will try to match the input with the `trigger` from the transition.
If such a match is found the `transition_fn` is called with a `%SimpleStateMachine.Match` struct which will give access
to the following fields:
%SimpleStateMachine.Match{
input: "The element from the input list",
data: "Injected value when the state machine is run, like an accumulator in Enum.reduce",
matched: "A value depending on what kind of trigger was used"}
The return value of the `transition_fn` will be injected into the `Match` struct's `data:` field for the next
loop.
#### Types of triggers
* Function triggers
...are the most versatile triggers, when a function trigger triggers on an input it returns an unfalsy
value that is passed into the `Match` struct's `matched:` field.
* Regex triggers
...can, obviously, only be used with `String` inputs. The result of `Regex.run(trigger, input)` is passed into
the `Match` struct's `matched:` field.
* `true` trigger
... matches always, `true` is passed into the `matched:` field.
* `:end` trigger
... does never match, however its associated `transaction_fn` is called, and its result will bet the result
of the machine's run. See also the `end:` state below.
#### Special States
Two states are special in the sense that their names are fixed.
* `:start` state
Is defined like any other state but is the machine's initial `current_state`. It is **obviously** necessarily
present in the `transition_map`.
* `:end` state
Can only have one `transition_fn` which is invoked at the end of the input list.
#### Reserved States
* `:halt` state
No transition definitions for this state will ever be read. If the `current_state` of the machine becomes
the `:halt` state, it stops and returns the `Match` struct's `data:` field.
No `:end` state or trigger treatment is performed.
* `:error` state and states starting with `:_`
Reserved for future use.
### Some Detailed Examples
Let's start with a single state machine.
iex(0)> parse_and_add = fn(string, data) ->
...(0)> {n, _} = Integer.parse(string)
...(0)> %{data|sum: data.sum + n} end
...(0)> add_error = fn(%{input: input, data: data}) ->
...(0)> %{data|errors: [input|data.errors]} end
...(0)> states = %{
...(0)> start: [
...(0)> {~r(\d+), fn %{matched: [d], data: data} -> parse_and_add.(d, data) end},
...(0)> {true, add_error},
...(0)> ],
...(0)> end: fn %{data: %{errors: errors, sum: sum}} -> {sum, Enum.reverse(errors)} end }
...(0)> run(~w{12 error 30 incorrect}, %{sum: 0, errors: []}, states)
{42, ~w(error incorrect)}
If the data is initially nil it needs not be passed into `run` and if the `transaction_fn` is a nop, it can be designated
by `nil`.
iex(1)> states = %{
...(1)> start: [
...(1)> { ~r{(\d+)}, fn %{matched: [_, d]} -> d end, :halt },
...(1)> { true, nil } ]}
...(1)> run(~w{ hello 42 84 }, states)
"42"
The difference between `:halt` and `:end` can be demonstrated with these slighly modified machines
iex(2)> sm1 = %{
...(2)> start: [
...(2)> { ~r{(\d+)}, fn %{matched: [_, d]} -> d end, :halt },
...(2)> { true, nil } ],
...(2)> end: fn %{data: x} -> {n, _} = Integer.parse(x); n end }
...(2)> sm2 = %{
...(2)> start: [
...(2)> { ~r{(\d+)}, fn %{matched: [_, d]} -> d end, :end },
...(2)> { true, nil } ],
...(2)> end: fn %{data: x} -> {n, _} = Integer.parse(x); n end }
...(2)> { run(~w{ hello 42 84 }, sm1), run(~w{ hello 42 84 }, sm2) }
{"42", 42}
So far we have only seen `Regex` and `true` triggers, the next example uses function triggers
iex(3)> odd? = &(rem(&1, 2) == 1)
...(3)> states = %{
...(3)> start: [
...(3)> {odd?, fn %{input: n, data: sum} -> sum + n end},
...(3)> {true} ] }
...(3)> run(1..6|>Enum.into([]), 0, states)
9
Some might suggest that the `{true}` transition should be a default, but we prefer to raise an error
if no transition matches
iex(4)> odd? = &(rem(&1, 2) == 1)
...(4)> states = %{
...(4)> start: [
...(4)> {odd?, fn %{input: n, data: sum} -> sum + n end} ]}
...(4)> run(1..6|>Enum.into([]), 0, states)
** (RuntimeError) No transition found in state :start, on input 2
An even more obvious exception is raised if a state has no transitions defined, that holds for the predefined
`:start` state as for any other state.
iex(5)> states=%{}
...(5)> run(~w[alpha beta], states)
** (RuntimeError) No transitions defined for state :start
iex(6)> states=%{
...(6)> start: [
...(6)> {true, nil, :second} ]}
...(6)> run(~w[alpha beta], states)
** (RuntimeError) No transitions defined for state :second
## Author
Copyright © 2019 Robert Dober, mailto:robert.dober@gmail.com
## License
[Apache-2.0](LICENSE)
<!-- SPDX-License-Identifier: Apache-2.0 -->