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!