Metaprogramming Fibonacci

10 Apr 2013

In a recent pairing session, a Ruby veteran showed me how to use metaprogramming to abstract a memoizing functionality out of a recursive Fibonacci method. Let's look at how that works.

First, we will start with a basic recursive Fibonacci method whose use of memoization depends upon a class variable.

class Fib
  @@memo = { 0 => 0, 1 => 1 }

  def self.fib_rec(n)
    @@memo[n] ||= fib_rec(n - 1) + fib_rec(n - 2)
  end
end

The code above is concise, fast, and clear. In my mind, it demonstrates both the power and beauty of Ruby. Abstracting the memo using metaprogramming serves only to emphasive the expressiveness of the language.

Let's start by removing the memo from our current method and converting the method itself into an instance method. The reason for switching from a class method will become clear in a moment. At the same time, we will mixin a Memo module, which will contain the memoize function.

class Fib
  extend Memo

  def fib_rec(n)
    return n if n < 2
    return fib_rec(n - 1) + fib_rec(n - 2)
  end

  memoize :fib_rec
end

Now we need to create the Memo module and define memoize. The method will need to do two things. It will redefine whatever method it receives so as to use memoization. And it will create an alias so that the unmemoized method is still callable. One implementation following such requirements is as follows:

module Memo
  @@memo = { 0 => 0, 1 => 1 }

  def memoize(method)
    alias_method "old_#{method}".to_sym, method

    define_method(method) do |*args|
      @@memo[args] ||= self.send("old_#{method}".to_sym, *args)
    end
  end
end

Although the memoize function looks complicated, it is quite simple. First, alias_method simply prefixes "old" before whatever method is passed in so that we can still call the old method. Second, with define_method we dynamically define a new method with memoization built in. Our new method still calls the old method whenever our memo does not hold a value for a particular key. Note that define_method will define an instance method in the receiver. Hence the need to switch from a class method above.

In all likelihood, memoize could use refactoring. Nonetheless, it gets the job done and demonstrates a basic use of metaprogramming with dynamic method assignments.

Update 13 April 2013

Charlie Somerville has proposed an improvement for the memoize method on Twitter.

module Memo
  def memoize(method)
    old_method = instance_method(method)
    memo = {}

    define_method(method) do |*args|
      memo[args] ||= old_method.bind(self).call(*args)
    end
  end
end

There are three key changes. First, instead of using alias_method, we have instance_method. Presumably, alias_method does not protect against a user altering the alias. By using instance_method instead, we have an unbreakable hold on the old method.

Second, the class variable memo has been removed in favor of a local hash. Why does this work? Because the memo object is referenced inside a block which behaves much like a closure, memo will persist throughout the lifetime of the program and will remain available for lookup.

Finally, we have to bind our instance method to a class. By passing self to bind, we are attaching our new method to the class of whatever method was passed in. The final step is to simply call the method with the appropriate arguments.

The result is a much tighter implementation of memoize.

comments powered by Disqus