torsdag, maj 29, 2008

Ruby closures addendum - yield

This should probably have been part of one of my posts on closures or defining methods, but I'm just going to write this separately, because it's a very common mistake.

So, say that I want to have a class, and I want to send a block when creating the instance of this class, and then be able to invoke that block later, by calling a method. The idiomatic way of doing this would be to use the ampersand and save away the block in an instance variable. But maybe you don't want to do this for some reason. An alternative would be to create a new singleton method that yields to the block. A first implementation might look like this:
class DoSomething
def initialize
def self.call
yield
end
end
end

d = DoSomething.new do
puts "hello world"
end

d.call
d.call
But this code will not work. Why not? Because as I mentioned in my post about defining methods, "def" will never create a closure. Why is this important? Well, because the current block is actually part of the closure. The yield keyword will use the current frames block, and if the method is not defined as a closure, the block invoked by the yield keyword will actually be the block sent to the "call" method. Since we don't provide a block to "call", things will fail.

To fix this is quite simple. Use define_method instead:
class DoSomething
def initialize
(class << self; self; end).send :define_method, :call do
yield
end
end
end

d = DoSomething.new do
puts "hello world"
end

d.call
d.call
As usual with define_method we need to open the singleton class, and use send. This will work, since the block sent to define_method is a real closure, that closes over the block sent to initialize.

Inga kommentarer: