From: Zellyn Hunter Date: 2003-06-19T09:11:57+09:00 Subject: Code generation and state machines 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) #