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)
#