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:

 1 class Unmemoized
 2     def factorial(n)
 3         puts n
 4         if n < 1
 5             1
 6         else
 7             n * factorial(n-1)
 8         end
 9     end
10 end
11 
12 unmemoized = Unmemoized.new
13 
14 5.downto(1) { |i| puts "\t#{unmemoized.factorial(i)}" }

…and the memoized version:

 1 class Memoized
 2     attr_reader :factorial_memo
 3     def initialize
 4         @factorial_memo = {}
 5     end
 6 
 7     def factorial(n)
 8         puts n
 9         unless @factorial_memo.has_key? n
10             if n < 1
11                 @factorial_memo[n] = 1
12             else
13                 @factorial_memo[n] = n * factorial(n-1)
14             end
15         end
16 
17         @factorial_memo[n]
18     end
19 end
20 
21 memoized = Memoized.new
22 
23 5.downto(1) { |i| puts "\t#{memoized.factorial(i)}" }
24 
25 puts memoized.factorial_memo.inspect

Printing the hashtable is especially telling: {5=>120, 0=>1, 1=>1, 2=>2, 3=>6, 4=>24} It reads like a look-up table for factorials.

Memoization in Facets

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. Facets makes memoization trivial, and removes all these issues.

 1 require 'facets/memoize'
 2 
 3 class FacetsMemoized
 4     def factorial(n)
 5         puts n
 6         if n < 1
 7             1
 8         else
 9             n * factorial(n-1)
10         end
11     end
12 
13     memoize :factorial # <= HINT
14 end
15 
16 facets_memoized = FacetsMemoized.new
17 
18 5.downto(1) { |i| puts "\t#{facets_memoized.factorial(i)}" }

In case you missed it, this is just like Unmemoized above, except we added line 13, memoize :factorial…that’s it. Just like attr_reader and friends, you can pass a list of symbols to memoize, and it’ll work on functions with any number of arguments:

 1 require 'facets/memoize'
 2 
 3 class MemoizedMath
 4     def add(n, m)
 5         n + m
 6     end
 7     def mult(n, m)
 8         n * m
 9     end
10     memoize :add, :mult
11 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 _N_th 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.