If you find yourself running a slow method multiple times in a single request where the output is consistent you might want to consider caching its return value using a technique called Memoization. Below is a screenshot from a simple example app which executes a method and displays its output and the time it takes to process.

We call this method twice and in a view template and it takes half a second to run each time. The method itself just sleeps for half a second to simulate a complex operation then returns a value. We’ll cache this value using memoization.

When we first covered this topic we used the ActiveSupport::Memoizable module but this has since been deprecated. The deprecation message tells us to “simply use Ruby memoization pattern instead”. The most common approach to doing this is to store the result in an instance variable which we can do like this:

/app/models/product.rb

deffilesize@filesize ||= begin
sleep 0.54815162342endend

As we’re caching the result of more than one line of code here we’ve used a beginend block here along with ||= to store the result in the instance variable the first time the method is called. When we reload the page now the method still takes half a second to run the first time but runs almost instantly the second time.

Using a begin block like this isn’t the best approach especially if the logic inside the block is more complex. In these scenarios it’s better to move the code out into a separate method like this:

Now that we have the code cleaned up a little we’ll focus on the ||= operator. How does this work? The line @filesize ||= calculate_filesize is roughly equivalent to @filesize || @filesize = calculate_filesize. This returns the value of the @filesize if it isn’t nil or false. Otherwise it calculates the value and stores the result in @filesize. The two forms aren’t exactly equivalent, however, there are some minor differences between them. We’ll investigate these by using irb with warnings turned on. If we try using the long form to return or set an instance variable we’ll get a warning telling us the the instance variable hasn’t been initialized. If we use the short ||= form we won’t see a warning.

Something we should always keep in mind when using this approach is that the value won’t be read from the cache if it is equal to false or nil. If we have the calculate_filesize method return one of these values it’s called twice when we reload the page and the page still takes a full second to load. To get around this problem we can set the cache value unless it has already been defined, like this:

Now the page caches the value the first time it’s calculated and the second time it’s read from the cache correctly.

Handling Methods That Take Arguments

We probably won’t run into this scenario very often so the ||= operator will generally work well enough. Another scenario we might encounter is that when the returned value is dependent on an argument passed into the method. We can demonstrate this by having calculate_filesize take an argument.

When we reload the page now the final snippet has the correct value and takes half a second to run as it’s being calculated rather than being fetched from the cache.

If we take this approach we should keep the arguments that are passed in as simple objects so that they work as hash keys. While we’re on the subject of hashes we’ll show you another approach to memoization which uses the hash default value. We can call Hash.new and pass in a block which will be executed when the key isn’t found. When we set a hash key with a given argument it will trigger a miss which will execute the block and cause the value to be cached. What’s nice about this approach is that if the method returns a false or nil value it will be cached and used again if the method is called with the same argument again.

A More General Solution

For fun let’s see what it takes to generalize this memoization approach so that we can easily add it to any method. We want to be able to revert to just having our filesize method again and to have a memoize class method that we can pass a method name to memoize it.

Our new memoize method takes the name of the method as an argument and the first thing we do is alias the method so that we can use it later. Next we define a method with that name which overrides the existing method in the class. This overriding method has a @_memoized instance variable which defaults to a new hash. We then create a new hash key with the same name as the method so that each memoized method can have its own key, and we store the value by setting a nested key with a key that’s the name we’ve passed in and with a value that’s determined by calling the original version of the method we’ve overridden. When we reload the page now to see if this works we see the same results as before with the second call to filesize without any arguments being cached as we’d expect.

Doing this abstraction works as an exercise but it has limited practical use. There aren’t many situations where we’d want to do this kind of thing. Most of the time we want to craft the memoization around an application’s specific needs as different apps generally require different approaches. For our application the first approach we took is good enough and doesn’t involve a lot of code. If we do want a more general approach the Memoist gem is a direct extraction of ActiveSupport::Memoizable. This is more feature complete that what we showed in our abstraction and includes functionality such as expiring caches.

Speaking of expiring the cache if we do need to make a cache stale with our approach we can simply clear the instance variable where the cached values are stored. Expiring the cache isn’t usually necessary, however, as the cache only hangs around for the duration of the object and this is often only a dingle request. If we do want to cache something that sticks around for a longer period we should consider using Rails’ built-in caching system. Also keep in mind that Memoization isn’t free: we’re saving CPU cycles but at the cost of memory. If we’re storing cache values based on arguments this can quickly grow out of hand if the method is called with a large number of different arguments. When in doubt it’s best to profile and benchmark different scenarios and avoid premature optimization.