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 thssubject
, 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.