Recently, I talked about a faster, cheaper way to calculate Fibonacci numbers. One of the optimizations I made was to remember the value of each Fibonacci number: since F(7) is always 13, instead of recalculating it each time N=7, we can stuff 7 -> 13 into a look-up table for future reference. The function builds up a cheat-sheet, to avoid doing the re-work. It remembers.

This is called memoization, and it’s a nice way to trade memory for performance. But it only works when the function always returns the same answer for a given set of arguments — otherwise it’s first-in wins, forever. This property of a function, returning the same answer for the same args, is called referential transparency.

A Sample Implementation

There are lots of ways you could memoize a function. Hash tables are a natural choice, since they map a key to a value, just like functions map arguments to a value. Even if you implement it differently, a hash table is a good working model for memoization.

Let’s briefly consider factorials. The regular version:

class Unmemoized def factorial(n) puts n if n < 1 1 else n * factorial(n-1) end end end unmemoized = Unmemoized.new 5.downto(1) { |i| puts "\t#{unmemoized.factorial(i)}" }[/sourcecode] ...and the memoized version: [sourcecode language='ruby'] class Memoized attr_reader :factorial_memo def initialize @factorial_memo = {} end def factorial(n) puts n unless @factorial_memo.has_key? n if n < 1 @factorial_memo[n] = 1 else @factorial_memo[n] = n * factorial(n-1) end end @factorial_memo[n] end end memoized = Memoized.new 5.downto(1) { |i| puts "\t#{memoized.factorial(i)}" } puts memoized.factorial_memo.inspect[/sourcecode] Printing the hashtable is especially telling: <code>{5=>120, 0=>1, 1=>1, 2=>2, 3=>6, 4=>24}</code> It reads like a look-up table for factorials. <h4>Memoization in Facets</h4> As relatively easy as that example is, it has its drawbacks: we need to track our previous results in a separate variable, the memoization code is mixed up with the actual calculation (the part we care about), we can't easily use it with other functions, and the pattern only works for functions of one argument. <a href="http://facets.rubyforge.org/">Facets</a> makes memoization trivial, and removes all these issues. require 'facets/memoize' class FacetsMemoized def factorial(n) puts n if n < 1 1 else n * factorial(n-1) end end memoize :factorial # <= HINT end facets_memoized = FacetsMemoized.new 5.downto(1) { |i| puts "\t#{facets_memoized.factorial(i)}" }[/sourcecode] In case you missed it, this is just like <code>Unmemoized</code> above, except we added line 13, <code>memoize :factorial</code>...that's it. Just like <code>attr_reader</code> and friends, you can pass a list of symbols to <code>memoize</code>, and it'll work on functions with any number of arguments: require 'facets/memoize' class MemoizedMath def add(n, m) n + m end def mult(n, m) n * m end memoize :add, :mult end

When You Might Use Memoization, and What to Avoid

There are a number of places where this is useful: calculating a value by successive approximation, finding the path to the root node in an immutable tree structure, finding the Nth number in a recursively-defined series, even simple derived values (like ‘abc’.upcase). In general, a function is a good candidate if it only looks at its arguments (no global, class, or member variables, no files or databases) — especially if those arguments are immutable.

Relying on side-effects (printing to standard out, writing to a database or file, or updating a variable) in memoized methods is a bad idea: they’ll only happen the first time your method is called with those arguments, which is probably not what you intend. (Unless you’re printing the arguments to illustrate how memoizing works.) On the other hand, relying on side-effects is generally a bad idea anyway. Even if you don’t use a functional programming language, you can still benefit from minimizing state changes.

Further Reading

If memoization sounds interesting to you, you might like Oliver Steele’s article about memoizing JavaScript functions. If you’re curious about immutability, you might like this Joshua Bloch interview. If you’re interested in functional programming, there are worse places to start than the excellent Structure and Interpretation of Computer Programs. And of course, there’s more where that came from, in Ruby Facets.