Memoization in Ruby

Augusts Bautra - Jun 30 '22 - - Dev Community

Memoization is a simple and powerful performance optimisation technique that all Ruby developers should have full command of.
Here I'll go over three memoisation variants, in order of increasing difficulty:

  1. Naive ||=-based memozation
  2. Intermediate defined?-based memoization
  3. Advanced memoization for methods with parameters

1. ||=

A popular Ruby idiom, pronounced "or-equals", where assignment only happens if the lefthand side is falsey. Use this where you know that any potential assigned values will never be falsey.

def response
  # it is a common practice to store the memoized value in
  # an instance variable (ivar) of the same name as the method.
  @response ||= expensive_operation
end
Enter fullscreen mode Exit fullscreen mode

2. defined?-based memoization

A less known pattern that is capable of memoizing falsey values. Also useful if expensive_operation is a multiline statement.

def response
  return @response if defined?(@response)

  # NB, this is a regular assignment because
  # line above already did the cache check.
  @response = expensive_operation
end
Enter fullscreen mode Exit fullscreen mode

3. Memoization for methods with parameters

Sometimes it's useful to memoize several values, depending on the arguments given to a method. This approach can only be used if the values the method returns (and that are about to be memoized) are idempotent i.e. remain the same on repeat calls.

It's a good practice to name the ivar in plural of the method name.

Notice that memo_key must be something unique based on the arguments given. Using Object#hash is a general approach, but there can be others, for example, args can be of a known type and have a unique identifier returned by .id...

def response(arg1, arg2)  
  @responses ||= {}
  memo_key = "#{arg1.hash}_#{arg2.hash}"

  return @responses[memo_key] if @responses.key?(memo_key)

  @responses[memo_key] = begin
    puts "Doing expensive operation"
    expensive_operation
  end
end

> response(:a, :b)
Doing expensive operation
=> something
> response(:a, :b)
=> something

> response(false, false)
Doing expensive operation
=> something_else
> response(false, false)
=> something_else
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .