Wednesday, December 16, 2009

Ruby and Simple Dynamic Programming

I've been reading"Ruby Best Practices" by Gregory T. Brown and it's well worth checking out for anyone interested in Ruby. I'm in the middle of Chapter 3, "Mastering the Dynamic Toolkit" and thought I'd share a some simple bits of code that actually use dynamic programming.

We actually make use of the fact that with Ruby you can add a method_missing method to any class that allows you to capture calls to any method that are ... well missing. Here's a simple example that shows how it works.

# Create a class with the method_missing method. If a method 
# is called on this class and it's not found, then this method
# will be called. We'll then print whatever was passed in along
# with the fact that it's not there.
class MM
def x
puts "Called x"
end

def y
puts "Called y"
end

def method_missing(name, *args, &block)
puts "You tried to call #{name} with #{args.inspect}. There is no method with that name."
end
end

if __FILE__ == $PROGRAM_NAME

# Create a new MM
mm = MM.new

# Call the two mehtods that do exist.
mm.x
mm.y

# Call z (which doesn't exist) both with and without
# paramaters.
mm.z
mm.z 1, 2, 3
end



This code creates a simple class called MM with three methods, x, y, and method_missing. The first two only print out the fact that they were called and the third will print out any other method that gets called, say z 1,2, 3 and will print out the name and parameters that get passed in to it. The main code just creates a new MM and calls a few methods (existing and not) on it.

Admittedly, this isn't too awfully interesting, but in our next example, we'll actually use method_missing to save ourselves some work. Here's the second bit of code:

# Create a class MM1. The x and y methods require calling
# setup before and teardown after. This means that everytime
# we have to add a new method (say z()), we end up having to
# duplicate this code.
class MM1

def setup
puts "Setup"
end

def teardown
puts "Tear down"
end

def x
setup
puts "Called x"
teardown
end

def y
setup
puts "Called y"
teardown
end
end

# Create a class with the method_missing method. Here we
# still require x and y to have setup and teardown called, but
# we wrap it in method_missing as setup_x_teardown or setup_y_teardown.
# This allows us to also add z without having to worry about remembering
# the setup/teardown.
class MM2

def setup
puts "Setup"
end

def teardown
puts "Tear down"
end

# The method_missing() does all of the work here. We check if the
# name is setup_something_teardown and if it is, we call setup,
# call something with the parameters passed in, and then call teardown.
# If the method isn't of this form, we just pass it along.
def method_missing(name, *args, &block)
case (name)
when /^setup_(.*)_teardown/
setup
send($1, *args, & block)
teardown
else
super
end
end

def x
puts "Called x"
end

def y
puts "Called y"
end
end

if __FILE__ == $PROGRAM_NAME

# Create a new MM1
mm1 = MM1.new

# Call the two mehtods that do exist.
mm1.x
mm1.y

# Create a new MM2
mm2 = MM2.new
mm2.setup_x_teardown
mm2.setup_y_teardown
end



There's two classes here that both "do" the same thing. The first class MM1 has two methods, x and y that require a setup before they are called and a teardown after they are called.

The second class, MM2, has the same requirements, but we're going to handle them a bit differently. We have the same setup and teardown, but we don't have x and y call them directly. Instead we use method_missing to handle the setup and teardown. What we're going to do is use method_missing and then check the name parameter. If it matches something that looks like setup_X_teardown, then we'll call setup, call the method using send, and finally, call teardown. If we don't match the pattern, then we just pass the call up the chain. The main code here creates both an MM1 and an MM2 and shows a bit of how to use them.

Here, in this simple example, you probably wouldn't bother with this. In the book Brown gives a much better use case, but this should give you some ideas at least.

Let me know if you have questions or comments.

1 comment:

  1. Generally for things like your setup / teardown example, I prefer a block interface:

    class Foo

    def process
    setup
    yield
    teardown
    end

    # ...

    end

    obj = Foo.new
    obj.process { obj.do_something }

    Thanks for mentioning RBP!

    ReplyDelete