Hi folks,
I’m starting a project for fun in Ruby, partly to help learn the language.
One of the pieces it needs is a state machine.
I liked the code generation for tables and rows described in
http://www.codegeneration.net/tiki-read_article.php?articleId=9 so I thought
I’d take a stab at getting it to work on a simple level. (By the way, don’t
ever write an article where you show the syntax of a really neat trick but
leave the implementation out because it’s “too complicated”. That’s just
taunting!)
Here’s my attempt. It actually works, but I wanted to get the list’s
feedback.
Mostly I just want to know whether there are cleaner ways to do any parts of
it, or if there is a more Ruby way to do any of it. Any criticism is
welcome.
The thing I’m least happy with is that I needed to create accessors for the @@
class variables because I couldn’t figure out how to reference the child
class’s class variables from the run() method defined in the parent class.
Is there a neat way to do it?
Thanks,
Zellyn
···
Parent class of state machines. Defines the functions that allow
simple syntax in child classes
class StateMachine
Create a new state - simply add the given block to the states hash
under the state’s name
def StateMachine.state(name, &action)
module_eval <<-"end_eval"
puts “DEBUG: Defining state ‘#{name}’”
@@states || @@states = {}
@@states[name] = action
end_eval
end
Create a new transition. Each start state’s entry in the
transitions hash is an array of pairs. Each pair contains
and end state and a condition block
def StateMachine.transition (startState, endState, &condition)
puts "DEBUG: Defining transition from ‘#{startState}’ to ‘#{endState}’"
module_eval <<-"end_eval"
ary = @@transitions[startState] || []
ary.push([endState,condition])
@@transitions[startState] = ary
end_eval
end
set the start state
def StateMachine.startstate(name)
module_eval <<-“end_eval”
@@startState = name
end_eval
end
set the end state
def StateMachine.endstate(name)
module_eval <<-“end_eval”
@@endState = name
end_eval
end
Actually run the state machine.
- Start in the start state.
- Evaluate each state’s block on entering the state
- Try each transition for the start state. When a condition
evaluates to true, enter the corresponding target state.
- Quit when you reach the end state
def run
currentState = startState()
while (currentState != endState()) do
states()[currentState].call()
ary = transitions()[currentState]
ary.each do |(target,condition)|
if condition.call()
currentState = target
next
end
end
end
# and execute the final state's action
states()[currentState].call()
end
Trap child classes inheriting from this class, and add the
necessary class variables and their accessors.
def StateMachine.inherited(subclass)
subclass.module_eval <<-“end_eval”
@@states = {}
@@transitions = {}
@@startState = nil
@@endState = nil
def states
@@states
end
def transitions
@@transitions
end
def startState
@@startState
end
def endState
@@endState
end
end_eval
end
end
First simple state machine:
Start -> Second -> End
class SimpleState1 < StateMachine
state(“Start”) { puts “Start State (1)” }
state(“Second”) { puts “Second State (1)” }
state(“End”) { puts “End State (1)” }
startstate(“Start”)
transition(“Start”, “Second”) { 1 }
transition(“Second”,“End”) { 1 }
endstate(“End”)
end
require ‘StateMachine’
Second simple state machine
Start -> Second -> Third -> End
(with never-taken transition from Second -> End)
class SimpleState2 < StateMachine
state(“Start”) { puts “Start State (2)” }
state(“Second”) { puts “Second State (2)” }
state(“Third”) { puts “Third State (2)” }
state(“End”) { puts “End State (2)” }
startstate(“Start”)
transition(“Start”, “Second”) { 1 }
transition(“Second”,“End”) { 0 }
transition(“Second”,“Third”) { 1 }
transition(“Third”,“End”) { 1 }
endstate(“End”)
end
Test it all out. Make two state machines with similar state names so
that we’re sure we’re actually defining the states and transitions
in the right place - overlaps/clashes will show up clearly.
machine1 = SimpleState1.new()
machine2 = SimpleState2.new()
puts()
puts "Running State Machine 1:"
machine1.run
puts()
puts "Running State Machine 2:"
machine2.run
puts()
puts "Running State Machine 1 again:"
machine1.run