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
#
# Output:
#
# DEBUG: Defining state 'Start'
# DEBUG: Defining state 'Second'
# DEBUG: Defining state 'End'
# DEBUG: Defining transition from 'Start' to 'Second'
# DEBUG: Defining transition from 'Second' to 'End'
# DEBUG: Defining state 'Start'
# DEBUG: Defining state 'Second'
# DEBUG: Defining state 'Third'
# DEBUG: Defining state 'End'
# DEBUG: Defining transition from 'Start' to 'Second'
# DEBUG: Defining transition from 'Second' to 'End'
# DEBUG: Defining transition from 'Second' to 'Third'
# DEBUG: Defining transition from 'Third' to 'End'
#
# Running State Machine 1:
# Start State (1)
# Second State (1)
# End State (1)
#
# Running State Machine 2:
# Start State (2)
# Second State (2)
# Third State (2)
# End State (2)
#
# Running State Machine 1 again:
# Start State (1)
# Second State (1)
# End State (1)
#