Memoization is the pattern of calculating a value once, and re-using that value each subsequent time it is needed.
It’s common to encounter this in Ruby in the form @variable ||= calculation
. It’s so common that it
is often used even where it’s not expensive or re-used; it’s become a part of idiomatic Ruby. In my
last post I made an off-hand reference to solving the problem
of using memoization for falsy values, and it seems a topic worth talking about in and of itself.
What do you mean there’s a problem?
Consider that the following methods have the same behavior:
When calculate_foo
returns a truthy object - anything but nil
or false
- there’s no problem at all. Calling any
one of those methods repeatedly will result in calculate_foo
only being called
once†.
When calculate_foo
returns a falsy object - nil
or false
- it stores that value. Every subsequent call will
invoke calculate_foo
another time and re-store the falsy value. If that calculation is expensive - makes a database
call, communications with an external API, or is otherwise-slow - then this is precisely the behavior that memoization
was intended to prevent (but didn’t).
The foolproof way to memoize
Now that we’ve identified the problem, how about a solution?
That’s all there is to it. Calling defined?(@foo)
checks whether the expression @foo
exists, though it is a bit
special since it does not actually evaluate the expression. It looks like a method, but is actually a Ruby keyword.
An alternative that uses a regular Ruby method is instance_variable_defined?(:@foo)
, but it’s a bit verbose. And
that’s actually the drawback to this foolproof approach in general. While it works the intended way and memoizes falsy
values, it’s longer, more-boilerplate, and less-readable.
How about a generalized approach?
Here’s a potential way to address this:
It’s worth pointing out that this approach only handles memoizing methods with no arguments. This could be adapted in order to handle arguments - in fact you can find some links in this articles footnotes that go into that.
There are several RubyGems that do this - and of course ActiveSupport
once had a memoizing module,
but I pretty much agree with the conclusion of the Rails team there, which is to say - it’s better to just use Ruby in
your own projects. It’s faster, it’s clearer, there’s no need for a dependency to do this. I’d even say this proposed
helper is overkill. The point was to show that it’s simple, and it’s something you can do if it makes your code clearer
or easier to work with.
Footnotes
† - this is not strictly true in a multi-threaded environment, but I’m choosing to avoid getting into that in this article.
This article is nothing new and has been written about before, but it’s a common-enough footgun that I didn’t mind writing up my version of this sentiment.