Caching Ruby Methods With Memoization

ruby

Have you ever been writing a piece of code, and had to do a lot of computing in one method? For whatever reason, you have a method that takes a long time to execute, or takes just a little bit of time, but needs to be called frequently with the same argument. Perhaps you run into this situation when you need to access the network in the body of the method. Or when calling the method requires instantiating a heavy library object, like an instance of a PDF reading library.

Memoization is the technical term for “caching the result of a method”. Instead of re-running the method body every time the method is called, we instead check to see if the result has already been calculated, and if so, just return that result. If we have not yet calculated the result, then we run the method to do the calculation and store the result for future use before returning it. This way we only need to spend the time to do the long-running operation once.

You can implement a memoization pattern for a zero-argument function easily in Ruby, using the same pattern that you could use in most any language like Java, Python, or even C:

1
2
3
4
5
6
7
class YourAwesomeClass
  def a_long_running_method
    return @cached_value if @cached_value
    @cached_value = do_some_stuff #This might be many lines!
    @cached_value
  end
end

Simple enough! This is the most literal translation of the recipe we described earlier - check to see if we calculated the value, return it if so, if not, calculate and store, then return.

Of course, there is a beautiful Ruby idiom we can use as well:

1
2
3
4
5
6
7
8
9
10
class YourAwesomeClass
  def a_long_running_method
    @cached_value ||= do_some_stuff
  end

  private # You can probably hide do_some_stuff
    def do_some_stuff
      # some stuff
    end
end

This relies on a few facts about ruby. First, every expression has a return value, and functions return the return value of their last expression by default. Hence, a_long_running_method will return the result of @cached_value ||= do_some_stuff. Second, ruby uses short-circuiting evaluation for ||, so if @cached_value is truthy (e.g. not nil), then @cached_value ||= do_some_stuff will immediately evaluate to true, and return @cached_value. And third, the string relies on ruby’s convenience syntax that makes @cached_value ||= do_some_stuff equivalent to @cached_value || @cached_value = do_some_stuff, where the variable assignment on the right side returns the value that was assigned.

It turns out the @cached_value ||= do_some_stuff pattern is so eloquent and common, and works so often, that Ruby on Rails, the most popular Ruby library, actually removed their custom memoization library, ActiveSupport::Memoization, in 2011. The pull request inspired some interesting discussion, and points out some major problems with the @var ||= pattern.

Most important to note is that sometimes you want nil to be an appropriate value for @cached_value, and the @var ||= pattern does not allow this. It will still return nil if do_some_stuff returns nil, but it requires running do_some_stuff every time, which might not be a good trade off. If you wanted to explicitly allow nil values, you would need to use a more verbose pattern like so:

1
2
3
4
5
class YourAwesomeClass
  def a_long_running_method
    @cached_value = do_some_stuff unless defined? @cached_value
  end
end

This way, we can check if @cached_value has ever been assigned - to nil or otherwise - instead of just checking if it is truthy.

There are some other gotchas, in particular with how instantiating Hashes work - if you want to pass a parameter and use it as a hash key for ac cached hash, you’ll need a couple more lines of code. But usually, @var ||= is sufficient.

Luckily, if you do need to do some more complex memoization, there is a gem called Memoist that extracts all the lost behavior from ActiveSupport::Memoizable and gives it back to you. With Memoist, you can pretty much ignore how the memorization takes and just trust the gem to do its job, using this pattern:

1
2
3
4
5
6
7
8
9
class Person
  extend Memoist

  def a_long_running_method
    do_some_stuff
  end
  memoize :a_long_running_method

end

Super simple! If you’re writing a lot of code that requires memoization, be sure to check it out - and watch out for those nil values when you’re using @var ||=!


I'm looking for better ways to build software businesses. Find out if I find something.