Invoking a Class block on an Instance

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::Document.new( tag_name )
end
end

def close_element
@current_parent = @current_parent.parent_node
end

add_state( :pre ){ |char|
@str = ''
case char
when WHITESPACE then :pre
when '<' then :eek:pen
end
}

add_state( :eek: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( :pre )
'<root> <foo /> </root>'.each_byte{ |c|
break unless parser.transition( c.chr )
}
 
P

Pit Capitain

Gavin said:
...
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).

Hi Gavin,

executing a code block in the context of an object is normally done via
instance_eval, but I, too, couldn't find a way to pass parameters to the
block.

One workaround is to create methods for the states. If you change the
following methods of FiniteStateMachine, it should work:

class FiniteStateMachine

...

def self.add_state( start_state, &block )
name = "__state_#{start_state}"
states[ start_state ] = name
define_method( name, &block )
end

...

def transition( trigger )
@state = send( self.class.states[ @state ], trigger )
end

end

Regards,
Pit
 
G

Gavin Kistner

--Apple-Mail-1--777588682
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed

executing a code block in the context of an object is normally done
via instance_eval, but I, too, couldn't find a way to pass
parameters to the block.

One workaround is to create methods for the states. If you change
the following methods of FiniteStateMachine, it should work:

Thanks for this; it solved my current problem.

I would still like to know how (if it's possible) I can do the Ruby
equivalent of this JS code:

var addSome = function( addend ){ return this.val + addend }

var obj1 = new Object;
obj1.val = 10;

var obj2 = new Object;
obj2.val = 100;

var sum1 = addSome.call( obj1, 10 ); //20
var sum2 = addSome.call( obj2, 10 ); //110

Is this the problem people are talking about when they say that Ruby
doesn't have "first class" functions/methods?
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,774
Messages
2,569,598
Members
45,147
Latest member
CarenSchni
Top