lab42_state_machine

A simple State Machine

Design Principles

  • A minimalistic but powerful API

  • Protection of the SM’s inner state through delegation from the API to the implementation

  • Run SM against Enumerables or Enumerators

  • Define a subject in the SM’s contsructor and run will return ths subject, defaulting to nil, in case you are aiming for side effects, but you are surely not, right ;)?

Examples

See also spec/acceptance/readme_spec.rb

A counter

```ruby require ‘lab42/state_machine’

Lab42::StateMachine.new 0 do # self.subject <– 0 state :init do self.subject += 1 end end.run %wb c # ===> 3 (self.subject) ```

This simple example demonstrates the following essential rules:

  • An easy way to setup the State Machine is by providing a block to its constructor. This block is instance evalled and thus has access to the whole API (see below). But of course the public API methods can be accessed in a more detached style too: state_machine.state :reset do @counter = 0 end

  • The initial state of the State Machine defaults to :init

The next example will demonstrate more API methods

```ruby SM = Lab42::StateMachine

  SM.new do
    # Before anything is run
    setup do
      @index = 0
      self.subject = Hash.new{ |h, k| h[k]=[] }
    end
    # After each transition
    after do
      @index += 1
    end
    
    # Before each transition
    before do | input |
      state input # sets state to input {true, false} in our case
    end

    # In this case input and new_state are known to be true of course
    transition init: true, false => true do | input, old_state, new_state |
      subject[true] << [@index]
      subject[false].last << @index if subject[false].last
    end

    # We show a different variation for the to false transitions here
    # much more elegant IMHO
    transition init: false do
      subject[false] << [0]
    end
    transition true => false do
      subject[false] << [@index]
      subject[true].last << @index
    end

    # There is no transition towards the end state (yet?).
    teardown do | last_state |
      subject[last_state].last << @index.succ if subject[last_state].last # Take care of empty input here
    end

  end.run [true, false, false, true, true, true, false] 
       # ===> { true => [[0,1],[3,6]], false => [[1,3], [6,7]] }   ```

In particular we are allowed to use instance variables (and their sexier accessor implementations) to transport state between the states of the State Machine. This is without danger as the API hides all behind the controller and does not contain any instance variable (not even @controller).

Of course it might be worthwile to consider closing over some local variables like in the following example if appropriate.

ruby # Sometime is is preferble to use the State Machine with a state implementing object result = SomeObject.new StateMachine.new do before do | input | state = some_function_of input end transition a: :b do | input | result.update from: :a, to: :b, with: :input end teardown do | last_input, old_state | result.update from: old_state, to: result.end, with: last_input end end.run ...

API

Setup Methods and Handler Definitions

The following methods can be used inside the constructor block without explicit revceiver, or just invoked on a StateMachine instance.

after

after do | input, old_state, new_state | will be run after each input record has been processed. As all handlers they will be executed in order of definition.

before

before do | input, old_state, new_state | will be run before each input record has been processed. As all handlers they will be executed in order of definition.

setup

setup do |input| The StateMachine peeks into the lazy enumerator and provides the first value (or nil for empty) as parameter. These handlers are the first to be run before any other and before the first input record will be fetched. Yet they give access to the Runtime API, notably they allow to set the initial state to something else as :init by means of the one parameter form to the state call state :new_initial_state

state

This is the (overloaded - for the sake of a slim API) workerbee of the State Machine, it comes in three forms:

state querying form (0 params)

Really part of the Runtime API

state currrent_state of the StateMachine

state setter form (1 param)

Really part of the Runtime API

state new_state You’ll never guess what this one does.

state handler definition form (1 param and block)

state some_state do |input, old_state, current_state| ... These blocks will be called for each input record processed in some_state

teardown

teardown do | last_input, last_state | Will be called at the very end of the processing cycle.

transition

transition a: :b, b: :c do |input, old_state, new_state| These are execute when the state changes from :a to :b, or :b to :a. Of course only one transition can be indicated (and mostly will be). These handlers are executed after the state handlers fot the new states, that is :b or :c in our case.

Runtime API

halt_machine

halt_machine stops the execution and makes run return the current subject

Raises a StopIteration and does therefore not make any sense in setup or teardown handlers

state

As already mentioned in the Definition and Setup API

This is the (overloaded - for the sake of a slim API) workerbee of the State Machine, it comes in three forms:

state querying form (0 params)

state Query currrent_state of the StateMachine

state setter form (1 param)

state new_state Set new state of the StateMachine

Advanced Stream API

This part of the API is exposed by controller and allows the StateMachine to implement some stream operations like rewind or drop_while.

Unless indicated otherwise the invocation of a method in this API does not interrupt the normal flow of the StateMachine but the bang version also implies a skip. Thusly in general

ruby controller.<advanced_stream_api_method>!

is the same as

ruby controller.<advanced_stream_api_method> skip

Also unless indicated otherwise the result of the invocation replaces the internal stream of the controller (having effect on input from the next step only)

In order to better understand this API we recommand to have a look at the corresponding specs

drop_until( &condition)

Works like drop_while{ |ele| !condition.( ele ) }

drop_while( &condition )

Forwarded to the internal stream which is replaced by this result.