Brian's Waste of Time

Sat, 08 Jan 2005

Futures, Part 2: Ruby

Okay, we looked at Java, which has futures built-in in 1.5, though rather non-transparent futures, compared to something like the referenced Alice. Took some time last night, and today during some breaks in some training (ah, companies which do in-house training are great things) I beat on transparent futures for Ruby. I'm not totally happy with how I detect access for lazy futures, but it does work, if with a bit of magic:

module Futures
    class Future

        def initialize(func, *args) 
            @func = func
            @args = args
            yield self
        end

        def evaluate
            @result = @func.call(self, *@args)
            @result.methods.each do |m|
                s = <<-EOS
                    def #{m}(*foos)
                        @result.send('#{m}', *foos) 
                    end
                EOS
                instance_eval(s) unless ['instance_eval','__id__','__send__'].member? m
            end
            self
        end
        
        def method_missing(symbol, args)
            evaluate unless available?
            @result.send(symbol, args) 
        end
        
        def available?
           defined? @result
        end
    end
    
    LAZY_SKIP_METHODS = ['instance_eval', '__id__', '__send__',
                          'evaluate', 'available?', '==', '===', 
                          '=~', 'method_missing']
    
    def lazy
        Future.new(proc { yield }) do |it|
            it.methods.each do |m|
                s = <<-EOS
                    alias __Futures_#{m} #{m}
                    def #{m}(*foos)
                        evaluate unless available?
                        __Futures_#{m}(*foos)
                    end
                EOS
                it.instance_eval(s) unless LAZY_SKIP_METHODS.member? m
            end 
        end
    end
    
    SPAWN_SKIP_METHODS = ['instance_eval', '__f_thread=', '__id__',
                          '__send__', 'evaluate', 'available?',
                          '==', '===', '=~', 'method_missing']
    
    def spawn
        f = Future.new(proc { yield }) do |it|
                
            def it.__f_thread=(t)
                @thread = t
            end
            
            it.methods.each do |m|
                s = <<-EOS
                    alias __Futures_#{m} #{m}
                    def #{m}(*foos)
                        @thread.join unless available?
                        __Futures_#{m}(*foos)
                    end
                EOS
                it.instance_eval(s) unless SPAWN_SKIP_METHODS.member? m
            end
        end
        Thread.start do 
            f.__f_thread=Thread.current
            f.evaluate
        end
        return f
    end
end

Note the two Future generating methods, spawn and lazy. Spawn will asynchronously evaluate the block, ~replacing the Future with the result upon completion, and blocking until completion if anything forces evaluation. Lazy will evaluate and replace the future when somethign forces evaluation. Both take a block from which the return value will ~replace the Future upon evaluation. Here's how they're used:

Lazy:

require 'futures.rb'
include Futures

class Wombat
    
    def initialize
       puts "initializing a Wombat" 
    end
    
    def say(x)
        # wombats sigh a lot
        puts "ahhhh #{x}"    
    end
end

thing = lazy { Wombat.new }
puts "here"
thing.say "nice"

Will produce the output:

here
initializing a Wombat
ahhhh nice

And the async usage is like:

class SlowWombat < Wombat
    def initialize
        puts "I am so slow..."
        sleep 2 
        puts "Ah, there I go..."
    end
end

other = spawn { SlowWombat.new }
puts "doing something while wombat wakes up..."
puts "gee, this is a slow wombat..."
other.say "uh huh"

Producing:

doing something while wombat wakes up...
I am so slow...
gee, this is a slow wombat...
Ah, there I go...
ahhhh uh huh

With a two second delay (had to run it a few times to get them nicely interspersed though ;-)

In both these examples I have the block instantiate a new object, but there is no need for that, whatever the block returns will ~replace the future when it is evaluated, so remote calls, etc, would be good candidates for async -- if you need the result before it gets back, you'll block on it when you try top get access it.

I have been saying ~replacing because the result of the evaluation isn't really replacing the future -- the future is delegating all calls to result when it becomes available, and doing other things before it becomes available (forcing lazy evaluation, waiting for the evaluation thread to finish). The method redefinition code was amazing fun to write. Maybe I really should learn Lisp macros...

Debugging this kind of absurdly dynamic code is fun, particularly when I forgot to exclude instance_eval from the forwarded methods and couldn't figure out why some methods were just not getting defined on the Future while others were. Oops.

All in all, GREAT fun, and a tool I am going to keep in my belt ;-)

0 writebacks [/src] permanent link