README.md

# 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 -->