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:
time: 5.000245s => "done" memoized processing time: 0.000041s => "done"memoized processing
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
to turn it off.
Alternatively can also use the
def memoized return @memoized if defined? @memoized @memoized = complex_computation end
There is one important difference between the two. If the return value is
||= 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:
time: 5.000333s => nil memoized_with_operator processing time: 5.004557s => nil memoized_with_defined processing time: 5.003176s => nil memoized_with_defined processing time: 0.000049s => nilmemoized_with_operator processing
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!