true, false & nil word cloud

Use Enhanced Memoization for false/nil with defined?

Memoization using the ||= operator is a useful and straightforward performance optimisation. However, this isn’t a suitable solution for cases when the expensive operation might result in false or nil.

Instead of …

…repeating potentially expensive calculations:

class OldTimeySweetShop
  def any_jars_nearly_empty?
    @any_jars_nearly_empty ||= glass_jars.any? do |jar|
      jar.count_each_sweet_by_hand < 10
    end
  end
end

Use…

…the defined? method with an early return.

class OldTimeySweetShop
  def any_jars_nearly_empty?
    return @any_jars_nearly_empty if defined?(@any_jars_nearly_empty)

    @any_jars_nearly_empty = glass_jars.any? do |jar|
      jar.count_each_sweet_by_hand < 10
    end
  end
end

Why?

Use of the #defined? method is technically the more “correct” way to perform memoization.

The ||= (or equals) operator literally means:

a || a = possibly_expensive_computation

A consequence of this is that if a is “false-y”, meaning set to nil or false, then the right-hand side of the || is executed. This is potentially a big issue because if the expensive computation is legitimately returning false, it will be run every time the method is used, completely circumventing the improvement we are attempting.

I’ve used Enumerable#any? above to illustrate that this defined?-based technique can be useful to improve performance for methods that loop over large datasets and might return a boolean or nil result.

Why not?

Often, a memoized result won’t be nil or false, and in that case this style is undoubtably more visually noisy to read, and possibly trickier to understand, when you come back to it later on.

Beware Threads!

You should be careful when using memoization in a high-throughput, threaded Ruby environment. You probably are. sidekiq is threaded and many Ruby web servers are as well.

When your code is reaching outside of the Ruby interpreter (e.g. reusable database connections, file handling, writing to a data store, manually creating background threads) it is possible to introduce race conditions if your code is called by multiple threads at the same time. However, you’re unlikely to be doing this in the course of a day-to-day application. Still, it is a possible source of bugs, so be aware!

Hat tip to Ivo on this one. He’s resolved bugs in open source projects that demonstrate this issue. You can see his recorded talk on spotting unsafe ruby patterns, which dives into the details.


last updated on 08 Feb 2021 by @andycroll

Don’t miss my next post, sign up to the One Ruby Thing email and get my next post in your inbox.

Don’t miss my next post, sign up to the One Ruby Thing email and get my next post in your inbox.