[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

Building a Better Functor

John W. Long

2/10/2005 5:38:00 AM

Hi,

I've been messing around a with functors lately (see [1] and [2]) and have found them really helpful for testing. They make great light-weight MockObjects. Consider the following example:

Pretend I have a method called redirect that I am trying to test:

def redirect(url, sys=Kernel)
#
# code here that writes out an HTTP header
# that causes the redirect to url
#

# my header has been written so kill the script:
sys.exit
end

Given the following definition for a functor I can easily mock out the sys.exit call:

class Functor
def initialize(method, &block)
@method = method
@block = block
end
def method_missing(symbol, *args)
if @method.to_s.intern == symbol
@block.call(*args)
else
super
end
end
end

Here's my test method:

def test_redirect__exit
exited = false
sys = Functor.new do |meth, *args|
exited = true if meth == :exit
end
redirect('test/url.html', sys)
assert(exited)
end

As you can see a functor is a pretty amazing little object. You can define method_missing a couple of different ways [3], but you are probably having your own ideas by now. Also note: because I'm using blocks my functor methods have access to variables that are in scope where the functor is defined (like "exited" in this case).

As I have used functors I have often found that there are times when it would be nice for a functor to support multiple methods--not just one. I've implemented seen this implemented a couple of ways, but haven't been totally satisfied with the results. Today, while talking to a colleague I hit upon an idea: why not allow functors to be added together to allow the construction of larger functor objects.

Consider the following definition for a functor:

class Functor
def initialize(method = nil, &block)
@methods = {}
add_method(method, &block) if block_given?
end

def __add__(f)
n = self.class.new
n.instance_variable_get(
'@methods'
).update(
@methods
).update(
f.instance_variable_get('@methods')
)
n
end

def add_method(method, &block)
@methods[method.to_s.intern] = block
end

def method_missing(symbol, *args)
case
when @methods.has_key?(symbol)
@methods[symbol].call(*args)
when symbol == :+
__add__(*args)
else
super
end
end
end

With an additional top level method:

def functor(method, &block)
Functor.new(method, &block)
end

This will allow you to do the following:

f = functor(:run) { puts 'running...' } +
functor(:jump) { puts 'jumping...' } +
functor(:play) { puts 'playing...' }

f.run #=> "running..."
f.jump #=> "jumping..."
f.play #=> "playing..."

This is a seriously cool concept. I'd like to see a version of this included in the standard lib. Perhaps even added to the core by modifying proc to behave the same manner (see Nowake's proposal[1]). Does anyone else have a better implementation? Can you see ways of improving my own?

I've uploaded my tests and source code on my Web site [4].

--
[1] http://groups-beta.google.com/group/comp.lang.ruby/browse_thread/thread/491add...
[2] http://rubyforge.org/pipermail/chicagogroup-members-list/2004-October/t...
[3] http://rubyforge.org/pipermail/chicagogroup-members-list/2004-October/0...
[4] http://johnwlong.com/download...

--
John Long
http://wiseheart...



28 Answers

John W. Long

2/10/2005 6:24:00 AM

0

John W. Long wrote:
> Here's my test method:
>
> def test_redirect__exit
> exited = false
> sys = Functor.new do |meth, *args|
> exited = true if meth == :exit
> end
> redirect('test/url.html', sys)
> assert(exited)
> end

er, sorry, this code should be:

def test_redirect__exit
exited = false
sys = Functor.new(:exit) do
exited = true
end
redirect('test/url.html', sys)
assert(exited)
end

--
John Long
http://wiseheart...



Jim Weirich

2/10/2005 7:26:00 AM

0

On Thursday 10 February 2005 12:37 am, John W. Long wrote:
> def __add__(f)

I'm almost certain you meant to say:
def +(f)

> This is a seriously cool concept. I'd like to see a version of this
> included in the standard lib. Perhaps even added to the core by modifying
> proc to behave the same manner (see Nowake's proposal[1]). Does anyone else
> have a better implementation? Can you see ways of improving my own?

It looks a bit like FlexMock (http://onest.../software...).

--
-- Jim Weirich jim@weirichhouse.org http://onest...
-----------------------------------------------------------------
"Beware of bugs in the above code; I have only proved it correct,
not tried it." -- Donald Knuth (in a memo to Peter van Emde Boas)


Robert Klemme

2/10/2005 11:03:00 AM

0


"John W. Long" <ng@johnwlong.com> schrieb im Newsbeitrag
news:420AF309.7090306@johnwlong.com...
> Hi,
>
> I've been messing around a with functors lately (see [1] and [2]) and
have found them really helpful for testing. They make great light-weight
MockObjects. Consider the following example:
>
> Pretend I have a method called redirect that I am trying to test:
>
> def redirect(url, sys=Kernel)
> #
> # code here that writes out an HTTP header
> # that causes the redirect to url
> #
>
> # my header has been written so kill the script:
> sys.exit
> end
>
> Given the following definition for a functor I can easily mock out the
sys.exit call:
>
> class Functor
> def initialize(method, &block)
> @method = method
> @block = block
> end
> def method_missing(symbol, *args)
> if @method.to_s.intern == symbol
> @block.call(*args)
> else
> super
> end
> end
> end

Just one remark: I'd change that to define the method in the constructor
and not use method_missing as that's more performant.

class Functor
def initialize(method, &block)
class << self;self;end.class_eval do
define_method(method,&block)
end
end
end

This variant does not allow for easy adding of methods via Functor#+
though. But you can easily define a method add(method,&block) that adds a
method and returns self for chaining:

class Functor
def add(method, &block)
class << self;self;end.class_eval do
define_method(method,&block)
end
self
end

alias :initialize :add
end

Functor.new(:x){puts "x"}.
add(:y){puts "y"}.
add(:z){puts "z"}.y


Kind regards

robert

John W. Long

2/10/2005 12:11:00 PM

0

Jim Weirich wrote:
> On Thursday 10 February 2005 12:37 am, John W. Long wrote:
>> def __add__(f)
>
> I'm almost certain you meant to say:
> def +(f)

Actually no. I used __add__(f) along this in method_missing:

> when symbol == :+
> __add__(*args)

Because I wanted people to be able to define their own behavior for plus:

f = functor (:+) { |value| 1 + value }

puts f + 1

--
John Long
http://wiseheart...



John W. Long

2/10/2005 12:15:00 PM

0

Robert Klemme wrote:
> This variant does not allow for easy adding of methods via Functor#+
> though. But you can easily define a method add(method,&block) that adds a
> method and returns self for chaining:
>
> class Functor
> def add(method, &block)
> class << self;self;end.class_eval do
> define_method(method,&block)
> end
> self
> end
>
> alias :initialize :add
> end
>
> Functor.new(:x){puts "x"}.
> add(:y){puts "y"}.
> add(:z){puts "z"}.y

Mmm, not sure. I really like the ability to use the + method. Seems like there should be a way to use "define_method" and still support the "+" method. Anybody have time to give it a go?

--
John Long
http://wiseheart...



Glenn Parker

2/10/2005 1:41:00 PM

0

Robert Klemme wrote:
> class << self;self;end.class_eval do

This particular trick has shown up a lot on the Ruby list lately. It's
kind of ugly, but clearly useful. I think it deserves a name, or
possibly a new keyword. What would you call it, "virtual_class_eval"?

--
Glenn Parker | glenn.parker-AT-comcast.net | <http://www.tetrafoi...


Gavin Kistner

2/10/2005 2:15:00 PM

0

On Feb 10, 2005, at 6:41 AM, Glenn Parker wrote:
> Robert Klemme wrote:
>> class << self;self;end.class_eval do
>
> This particular trick has shown up a lot on the Ruby list lately.
> It's kind of ugly, but clearly useful. I think it deserves a name, or
> possibly a new keyword. What would you call it, "virtual_class_eval"?

Apparently I haven't been paying attention. What does the above do, and
how does it differ from:

class Foo
def self.add( meth, &block )
self.class_eval{ define_method( meth, &block ) }
end
end

or

class Foo
def add( meth, &block )
self.class.class_eval{ define_method( meth, &block ) }
end
end

?

Is it to add new methods to the instance only, and not all instances
derived from Foo?
--
(-, /\ \/ / /\/



Robert Klemme

2/10/2005 2:34:00 PM

0


"Gavin Kistner" <gavin@refinery.com> schrieb im Newsbeitrag
news:991b24e67ac5d4902f4b04192210830a@refinery.com...
> On Feb 10, 2005, at 6:41 AM, Glenn Parker wrote:
> > Robert Klemme wrote:
> >> class << self;self;end.class_eval do
> >
> > This particular trick has shown up a lot on the Ruby list lately.
> > It's kind of ugly, but clearly useful. I think it deserves a name, or
> > possibly a new keyword. What would you call it, "virtual_class_eval"?
>
> Apparently I haven't been paying attention. What does the above do, and
> how does it differ from:
>
> class Foo
> def self.add( meth, &block )
> self.class_eval{ define_method( meth, &block ) }
> end
> end
>
> or
>
> class Foo
> def add( meth, &block )
> self.class.class_eval{ define_method( meth, &block ) }
> end
> end
>
> ?
>
> Is it to add new methods to the instance only, and not all instances
> derived from Foo?

Exactly.

robert

> --
> (-, /\ \/ / /\/
>
>
>

Gavin Kistner

2/10/2005 2:47:00 PM

0

On Feb 10, 2005, at 7:35 AM, Robert Klemme wrote:
> "Gavin Kistner" <gavin@refinery.com> schrieb im Newsbeitrag
> news:991b24e67ac5d4902f4b04192210830a@refinery.com...
>>> Robert Klemme wrote:
>>>> class << self;self;end.class_eval do
>> Is it to add new methods to the instance only, and not all instances
>> derived from Foo?
>
> Exactly.

Coming from Javascript (where every instance is trivially extensible
directly) that seems somewhat cumbersome.

Do many people write things like:

class Object
def instance_class
class<<self; self; end
end
end

in a common library they re-use frequently? Is there an RCR (or has
there been discussion) about adding a simple method like that to the
language, so you can simply do:

f = Foo.new
f.instance_class.class_eval{ ... }
?

Or (what I'm really looking for) perhaps it might be nice to have a
define_method method that worked directly for instances. Hrm, but what
would the syntax be? Perhaps that's not such a good idea.



Gavin Kistner

2/10/2005 2:57:00 PM

0

On Feb 10, 2005, at 7:47 AM, Gavin Kistner wrote:
> Or (what I'm really looking for) perhaps it might be nice to have a
> define_method method that worked directly for instances. Hrm, but what
> would the syntax be? Perhaps that's not such a good idea.

Well, no, maybe it would be nice. Something like:

class Object
def add_method( name, &block )
case self
when Class
self.class_eval{
define_method( name, &block )
}
else
class<<self; self; end.class_eval{
define_method( name, &block )
}
end
end
end

class Foo
def whee; puts "#{self} says 'whee'"; end
end

Foo.add_method( 'yow' ){ puts "#{self} says 'yow!'" }

f1 = Foo.new
f2 = Foo.new

f1.whee
#=> #<Foo:0x1cbdec> says 'whee'

f1.yow
#=> #<Foo:0x1cbdec> says 'yow!'

f2.add_method( 'booya!' ){ puts "#{self} says 'booya!'" }
f2.booya!
#=> #<Foo:0x1cbdc4> says 'booya!'

f1.booya!
#=> tmp.rb:33: undefined method `booya!' for #<Foo:0x1cbdec>
(NoMethodError)


I'm certainly adding that to my personal library, but it seems like it
would be a nice (useful, good, less-hacks-needed) built-in.


--
(-, /\ \/ / /\/