Ruby Memoization and Checking if Defined

Exploring the Implementation Details of Instance Variables

Memoization can be a useful tool in programming. The idea is to store the results of an expensive computation and then reference the stored result.

In Ruby 📖, you might see the following:

def memozied_result
  @memoized_result ||= expensive_calculation_for_results
end

The ||= operator does a bit of work, so let’s expand that to it’s logical equivalent:

def memozied_result
  return @memoized_result if @memoized_result

  @memoized_result = expensive_calculation_for_results
end

Consider what happens if expensive_calculation_for_results returns something “falsey” (e.g. nil or false); you’ll always call expensive_calculation_for_results. Which defeats the purpose of memoization.

Before we go further, we need to understand an implementation detail of Ruby’s instance variables.

Implementation Detail of Ruby’s Instance Variables

When we reference an undeclared instance variable, the returned value of that instance variable is nil. When we set an instance variable to nil, the returned value of that instance variable is nil.

Let’s fire up an IRB instance:

❯ irb
irb(main):001:0> @memoized_result
=> nil
irb(main):002:0> @memoized_result = nil
=> nil
irb(main):003:0> @memoized_result
=> nil

In other words, we don’t know if we’ve set @memoized_result or not. We need something different.

Enter Ruby’s defined? Method

Ruby’s defined? method returns the definition type of the object passed to the defined? method. It returns nil if it is not defined.

❯ irb
irb(main):001:0> defined?(@memoized_result)
=> nil
irb(main):002:0> @memoized_results = nil
=> nil
irb(main):003:0> defined?(@memoized_result)
=> "instance-variable"

The above example demonstrates that we can use defined? to help with our memoization quandry.

Let’s rewrite our method to use memoization that copes with the expensive computation returning nil:

def memozied_result
  return @memoized_result if defined?(@memoized_result)

  @memoized_result = expensive_calculation_for_results
end

The above is not as elegant as the terse @memoized_result ||= expensive_calculation_for_results.

A Rails Sidebar

Let’s expand just a bit and look at this through the lens of ActiveSupport’s #present? method; a helpful method that Ruby on Rails 📖 provides.

In the below example, I’m running Bundler’s console command with a Gemfile that requires ActiveSupport.

❯ bin/console
irb(main):001:0> nil.present?
=> false
irb(main):002:0> @memoized_result.present?
=> false
irb(main):003:0> defined?(@memoized_result)
=> nil
irb(main):004:0> @memoized_result = nil
=> nil
irb(main):005:0> @memoized_result.present?
=> false
irb(main):006:0> defined?(@memoized_result)
=> "instance-variable"
irb(main):007:0>

The #present? method does not check if the receiver is defined. It checks the value of the receiver. So we can’t rely on #present? when we really want defined?.

Conclusion

Memoization is intended to help cache potentially expensive computations. And Ruby’s elegant idioms can lead towards thinking something is memoized, when in fact we have to guard against the dread conceptual Null Pointer.