G
Gavin Kistner
--Apple-Mail-1--804272010
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed
For "fun" I started writing my own finite state machine system
tonight. (Or rather, what my idea of one is.)
I wanted to be able to subclass the FSM with specific classes that
define the states; each state is a combination of a unique object,
and a block that handles that state. Each time the state machine is
fed a new piece of input, it yields that information to the block for
the current state, and uses the return value as the next state.
I need a way to aggregate data as I crawl through the states, so I
thought I'd use instance variables and call some convenience instance
methods on my FSM subclass.
Unfortunately, I can't figure out how to get an instance of a class
to invoke a block stored in its parent class, using the scope of the
instance for the block. In JS this would be easy, but I don't yet
grok how to have Ruby run a set of code on any arbitrary scope (while
passing parameters to that code).
Code follows. I've pared down some of the (limited) bounds checks for
cleanliness, and removed most of the (undone) states. The problem
occurs when the :tag_start state tries to call the (instance)
"start_element" method, and complains that no such method exists on
the XMLParser class.
/Users/gavinkistner/Desktop/tmp.rb:61: undefined method
`start_element' for XMLParser:Class (NoMethodError)
class FiniteStateMachine
def self.states
@states ||= {}
end
def self.add_state( start_state, &block )
states[ start_state ] = block
end
def initialize( current_state=nil )
@state = current_state
end
def transition( trigger )
@state = self.class.states[ @state ].call( trigger )
end
end
class XMLParser < FiniteStateMachine
WHITESPACE = /\s/
STARTCHAR = /[a-z_]/i
NAMECHAR = /[a-z0-9_.-]/i
def start_element( tag_name )
if @doc
new_element = @doc.create_element( tag_name )
@current_parent.append_child( new_element )
@current_parent = new_element
else
@current_parent = MyXML:ocument.new( tag_name )
end
end
def close_element
@current_parent = @current_parent.parent_node
end
add_state( re ){ |char|
@str = ''
case char
when WHITESPACE then re
when '<' then pen
end
}
add_state( pen ){ |char|
case char
when STARTCHAR
@str << char
:tag_start
end
}
add_state( :tag_start ){ |char|
case char
when NAMECHAR
@str << char
:tag_start
when WHITESPACE
start_element( @str )
:attrs
when '>'
start_element( @str )
:chilluns
when '/'
start_element( @str )
:empty_tag
end
}
end
parser = XMLParser.new( re )
'<root> <foo /> </root>'.each_byte{ |c|
break unless parser.transition( c.chr )
}
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed
For "fun" I started writing my own finite state machine system
tonight. (Or rather, what my idea of one is.)
I wanted to be able to subclass the FSM with specific classes that
define the states; each state is a combination of a unique object,
and a block that handles that state. Each time the state machine is
fed a new piece of input, it yields that information to the block for
the current state, and uses the return value as the next state.
I need a way to aggregate data as I crawl through the states, so I
thought I'd use instance variables and call some convenience instance
methods on my FSM subclass.
Unfortunately, I can't figure out how to get an instance of a class
to invoke a block stored in its parent class, using the scope of the
instance for the block. In JS this would be easy, but I don't yet
grok how to have Ruby run a set of code on any arbitrary scope (while
passing parameters to that code).
Code follows. I've pared down some of the (limited) bounds checks for
cleanliness, and removed most of the (undone) states. The problem
occurs when the :tag_start state tries to call the (instance)
"start_element" method, and complains that no such method exists on
the XMLParser class.
/Users/gavinkistner/Desktop/tmp.rb:61: undefined method
`start_element' for XMLParser:Class (NoMethodError)
class FiniteStateMachine
def self.states
@states ||= {}
end
def self.add_state( start_state, &block )
states[ start_state ] = block
end
def initialize( current_state=nil )
@state = current_state
end
def transition( trigger )
@state = self.class.states[ @state ].call( trigger )
end
end
class XMLParser < FiniteStateMachine
WHITESPACE = /\s/
STARTCHAR = /[a-z_]/i
NAMECHAR = /[a-z0-9_.-]/i
def start_element( tag_name )
if @doc
new_element = @doc.create_element( tag_name )
@current_parent.append_child( new_element )
@current_parent = new_element
else
@current_parent = MyXML:ocument.new( tag_name )
end
end
def close_element
@current_parent = @current_parent.parent_node
end
add_state( re ){ |char|
@str = ''
case char
when WHITESPACE then re
when '<' then pen
end
}
add_state( pen ){ |char|
case char
when STARTCHAR
@str << char
:tag_start
end
}
add_state( :tag_start ){ |char|
case char
when NAMECHAR
@str << char
:tag_start
when WHITESPACE
start_element( @str )
:attrs
when '>'
start_element( @str )
:chilluns
when '/'
start_element( @str )
:empty_tag
end
}
end
parser = XMLParser.new( re )
'<root> <foo /> </root>'.each_byte{ |c|
break unless parser.transition( c.chr )
}