Memoization in Ruby

Memoization is useful when you have some code that is used multiple times to avoid computing it each time.

Suppose the following example:

class Employee

  def salary
    CalculateSalary.call(employee: self)
  end

  def salary_per_hour
    # for simplicity don't worry about leap years.
    salary / (365 * 8)
  end

  def salary_per_month
    salary / 12
  end

end

We want to calculate the employee's salary per month as well as per hour. We have an external service that calculates the total salary, we just need to divide this. However in the above code, the CalculateSalary will be called twice. This is not ideal, we want to memoize the value after the first call and use it for any consecutive cases.

There are multiple ways to achieve this.

||=

One is the conditional assignment operator ||= Here's an example:

def memoized
  @memoized ||= complex_computation
end

def complex_computation
  # doing something complex, takes a long time.
  sleep(5)

  'done'
end

So the first time we run the calculations, then on the second we just return the saved result:

3.0.0 :011 > memoized
processing time: 5.000245s
 => "done"
3.0.0 :012 > memoized
processing time: 0.000041s
 => "done"

Btw an easy measuring tool has been added to ruby 3 that I use here. just type measure, then every statement will return the time it took to be processed. Use measure :off to turn it off.

defined?

Alternatively can also use the defined? method:

def memoized
  return @memoized if defined? @memoized

  @memoized = complex_computation
end

There is one important difference between the two. If the return value is nil then ||= won't work, it will calculate every time. However, since the variable will be defined even with nil value, using the second approach memoizes it in this case as well.

def memoized_with_operator
  @memoized_with_operator ||= complex_computation
end

def memoized_with_defined
  return @memoized_with_defined if defined? @memoized_with_defined

  @memoized_with_defined = complex_computation
end

def complex_computation
  # doing something complex, takes a long time.
  sleep(5)

  nil
end

measure

memoized_with_operator
memoized_with_operator

memoized_with_defined
memoized_with_defined

running this results in:

3.0.0 :020 > memoized_with_operator
processing time: 5.000333s
 => nil
3.0.0 :021 > memoized_with_operator
processing time: 5.004557s
 => nil
3.0.0 :022 >
3.0.0 :023 > memoized_with_defined
processing time: 5.003176s
 => nil
3.0.0 :024 > memoized_with_defined
processing time: 0.000049s
 => nil

As we can see using the defined method cuts down the runtime significantly while the operator does not.

So my general advice is to use the ||= operator when the assignment is 1 line and the value returned is never nil. If it can be nil or it takes multiple lines, use the defined? method approach.

So going back to the first example it would look like this:

class Employee

def salary
  @salary ||= CalculateSalary.call(employee: self)
end

def salary_per_hour
  salary / (365 * 8)
end

def salary_per_month
  salary / 12
end

Have a great day and don't forget to be awesome!

#ruby
 
Share this