Friday, April 08, 2005

Lazy Collections In Ruby

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.

No comments: