Memoization is a liability

Written . Tagged Ruby.

This is another post extracted from discussions I’ve had on exercism.

It’s quite common to see memoization like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Word
  def initialize(string)
    @string = string
  end

  def letters
    @letters ||= @string.chars
  end

  def do_something
    10.times do |i|
      whatever(letters)
    end
  end
end

Since we’ll call letters multiple times, the code author stores away and reuses the value on the assumption that it’s more efficient.

I wouldn’t advise it.

Memoization is caching. Caching easily bites you in the ass. It’s another thing you have to keep correct as you update your complex system.

Say we add a #string= method to change that string at any time. Will you remember to clear the cache?

Say we add a #string reader and someone does word.string.upcase!. Or we add a word.upcase! method. The cached value is wrong again.

A more advanced memoization could handle that:

1
2
3
4
def letters
  @letters ||= {}
  @letters[@string] ||= @string.chars
end

But you probably didn’t think about that, because caching is hard. If you did think about it, you might find it clutters up your class, obscuring the actual logic.

Of course, caching can be very useful and the right thing to do. If your code has a performance issue, it may be better, all things considered, to trade away some simplicity for speed – guided by benchmarks. But if you don’t suspect a performance issue, don’t bother.

In many cases, your code with memoization is fast enough and more bug prone; your code without memoization is fast enough and less bug prone.