Memoization is a technique you can use to speed up your accessor methods. It caches the results of methods that do time-consuming work, work that only needs to be done once. In Rails, you see memoization used so often that it even included a module that would memoize methods for you.
Later, this was controversially removed in favor of just using the really common memoization pattern I’ll talk about first. But as you’ll see, there are some places where this basic pattern just doesn’t work right. So we’ll also look at more advanced memoization patterns, and learn some neat things about Ruby in the process!
Super basic memoization
You’ll see this memoization pattern all the time in Ruby:
The ||=
more or less translates to @twitter_followers = @twitter_followers || twitter_user.followers
. That means that you’ll only make the network call the first time you call twitter_followers
, and future calls will just return the value of the instance variable @twitter_followers
.
Multi-line memoization
Sometimes, slow code won’t fit on one line without doing terrible things to it. There are a few ways to extend the basic pattern to work with multiple lines of code, but this is my favorite:
The begin...end
creates a block of code in Ruby that can be treated as a single thing, kind of like {...}
in C-style languages. That’s why ||=
works just as well here as it did before.
What about nil?
But these memoization patterns have a nasty, hidden problem. In the first example, what if the user didn’t have a twitter account, and the twitter followers API returned nil
? In the second, what if the user didn’t have any addresses, and the block returned nil
?
Every single time we’d call the method, the instance variable would be nil
, and we’d perform the expensive fetches again.
So, ||=
is probably not the right way to go. Instead, we have to differentiate between nil
and undefined
:
Unfortunately, this is a little uglier, but it works with nil
, false
, and everything else. (To handle the nil
case, you could also use Null Objects and empty arrays to avoid this problem. One more reason to avoid nil
!)
And what about parameters?
We have some memoization patterns that work well for simple accessors. But what if you want to memoize a method that takes parameters, like this one?
It turns out that Ruby’s Hash
has an initalizer that works perfectly for this situation. You can call Hash.new
with a block:
Then, every time you try to access a key in the hash that hasn’t been set, it’ll execute the block. And it’ll pass the hash itself along with the key you tried to access into the block.
So, if you wanted to memoize this method, you could do something like:
And no matter what you pass into order_by
, the correct result will get memoized. Since the block is only called when the key doesn’t exist, you don’t have to worry about the result of the block being nil or false.
Amazingly, Hash
works just fine with keys that are actually arrays:
So you can use this pattern in methods with any number of parameters!
Why go through all this trouble?
Of course, if you start adding these memoization patterns to a lot of methods, your code will get pretty unreadable pretty quickly. Your methods will be all ceremony and no substance.
So if you’re working on an app that needs a lot of memoization, you might want to use a gem that handles memoization for you with a nice, friendly API. Memoist seems to be a good one, and pretty similar to what Rails used to have. (Or, with your newfound memoization knowledge, you could even try building one yourself).
But it’s always interesting to investigate patterns like this, see how they’re put together, where they work, and where the sharp edges are. And you can learn some neat things about some lesser-known Ruby features while you explore.