I ported my
Squeak Lazy Collections project to Ruby tonight and called it
Lazy Enumerable. It was amazingly a pretty much straight forward port. Basically, the point of the project is not to create a new collection every time you call select, collect, reject on a collection. It simply holds on to the block and the original collection. Now, the fun begins when you start nesting. For example:
temp=some_collection.select do |each|
each.is_interesting
end
answer=temp collect {|each| each.to_something}
Basically, the lazy enumerable combines the blocks for the collect and select into one and will not calculate the merged collection until some method like each or size is called. So, how do you do that? Well, the code for calling each on the LazyEnumerable looks like this:
def each(&proc)
self.original.each do |each|
if (self.monadic.nil?)
answer=each
else
answer=self.monadic.call(each)
end
proc.call(answer) unless answer === IGNOREABLE_OBJECT
end
end
I simply call each on the original collection and then call the block defined in myself (which I call monadic) that simply transforms values when called. Now, I return a special value if it should be ignored. This will be covered when you add a select block. For now, let's look at the simple case of adding a block that collects:
def add_collect(&proc)
add_monadic_valuable(&proc)
end
This passes straight through to this method:
def add_monadic_valuable(&proc)
current=self.monadic
if (current.nil?)
self.monadic=proc
return self
end
self.monadic=lambda do |each|
result=current.call(each)
if (result === IGNOREABLE_OBJECT)
result
else
proc.call(result)
end
end
self
end
The interesting part is when I set my instance variable, monadic. I'm basically combining the current monadic with the new one passed in. Pretty cool, huh? I think this is the kind of stuff that functional programmers love. I can see why. It feels right. Now, let's look at how to add a select block:
def add_select(&proc)
add_monadic_valuable do |each|
if (proc.call(each))
each
else
IGNOREABLE_OBJECT
end
end
end
All I do is call the same add_monadic_valuable method, but with a twist. I create yet another block that calls the select block inside to see if the value is true or false. If false, we pass back that IGNOREABLE_OBJECT. In the Smalltalk version, I originally threw an exception but took it out when the performance suffered. So, in short, I simply turn my select into a special case of collect! Now, how do I support rejects? Do I copy the code for select and use unless? Nope, all I do is negate the reject block before I send it into the add_select method. How do you do that? Like this:
class Proc
def negate
lambda {|each| !self.call(each)}
end
end
Yet another block trick that saves me duplicated code. And that's it! If you want to run and look at the code, feel free. Check it out
here. You can also download the Squeak version via SqueakMap. This was just a little project to play around with y combinator logic. It was cool looking at the Ruby and Smalltalk versions.