J
John W. Long
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].
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
functor
functor
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].