In this article you will read about classic form of memoization in Ruby, in order to present to you further approaches that meet certain requirements which the classical form can not afford.
Memo is a tool to achieve a significant increase in arbitration of arbitrary code with little effort by repeating repetitive calls from time-critical segments only once, and storing the result for all subsequent accesses in the memory. In addition the memoisation also plays a major role in functional languages.
Conditional assignment with || =
If you are dealing with Ruby or Ruby on Rails for a long time, the conditional assignment operator will sooner or later encounter: || = . Often this involves assigning a default value to a function argument, which is optional.
The intention here is to check whether b already has a value, and only if this is not the case, b assign the default DEFAULT_VALUE. Here we owe two properties: the conditional evaluation of Boolean expressions and the fact that nil is evaluated as false in Boolean expressions, and all objects except false and nil are evaluated as false.
Memoization by conditional assignment
When you hit a line similar to the following, for example in a Rails controller:
If you once read this as a conditional assignment, the meaning of such expression becomes somewhat clearer:
@_current_user has not yet been assigned, so is simply not yet defined. Therefore, the right part is executed and the user is then assigned @_current_user.
For all subsequent calls, the user has already been assigned @_current_user, so the instance variable is evaluated to true, and the right-hand assignment part is no longer running. This means that when a current_user is called, the possibly “expensive” user.find (session ) access must only be executed once, all subsequent accesses to current_user deliver the user object directly from the memory. In other words, the value of the instance variable is @_current_user.
If current_user z, for example, from a view, database access must occur only once, instead of repeating it again for each call.
This way, the meaning of memoization is a classic “time / memory tradeoff”: time-consuming calculations is made only ones and then stored in memory.
Caution with false and nil
What will happen if the value calculated in the initial assignment is false?
If we assume that expensive_calculation_resulting_in_false is calculated as false, then @_payment_required is also false and with further invocations of payment_required? The right part is executed again and again, although the instance variable has been assigned for memoization. The same problems naturally arise when the original calculation has nil as a result. What should you do in this case?
You have to analyze what is the actual criterion, according to which we decide whether the assignment has already been carried out or not. Basically, it has nothing to do with the value that results from the initialization. In the first pass @_payment_required is just defining. The instance variable is already defined for all subsequent calls. Fortunately, Ruby offers us to check this case:
defined? is actually an operator and not a method. For example, the existence of local variables that might not have been declared:
Even if in the form with the conditional assignment it not appears as brief and concise, in practice the variant should be defined with ?.
When to use Memo?
It’s clear that the benefits of memoization are not revealed in code part which is stored and called only ones. If you want to memo a method with arguments, this makes sense only if the set of possible arguments remains manageable. If these are different in the majority, the effect of the memoization is hardly applicable.
In the context of Rails, for example, you should be aware that most objects are only for the duration of a single request. Memoization is only worthwhile if the memoized part is called several times during the same request.
Danger of storage leaks in long-lived objects
You have to pay attention when an object using Memo is referenced by other objects. If one of the referencing objects, for example, beyond request limits, there is a risk of creating a memory leak. Although the referenced object that implements memo-lisation may no longer be needed, it is still not captured by the garbage collector because the reference is still valid within the long-lived object. The same can happen when the memo object itself is a global singleton instance or have constant request boundaries.
Since USER_CACHE is never detected as a constant by the garbage collector, more and more user objects are collected in USER_CACHE and the runtime increases. At the same time more and more memory is consumed which leads to the NoMemoryError.
This article is a translated summary of the original article on www.informatik-aktuell.de written by Martin Boßlet.