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

  require 'lab42/state_machine'

  Lab42::StateMachine.new 0 do # self.subject <-- 0
    state :init do
      self.subject += 1
    end
  end.run %w{a b 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

      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.

   # 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

    controller.<advanced_stream_api_method>!

is the same as

    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.