Memoization is a technique employed in many languages which allows you to cache the results of slow or expensive operations and return the cached result whenever the method is subsequently called. I don’t think there’s a single application I’ve written which doesn’t make use of memoization in one way or another. The most basic (and most familiar) technique in Ruby is like this:
class User
def latest_tweet
@latest_tweet ||= TwitterAPI.latest_tweet_for(self.twitter_username)
end
end
Calling the latest_tweet method for a user will now either return the value stored in the @latest_tweet
. The first call will possibly take a few seconds while it downloads from the Twitter API but any subsequent calls for the same instance of User will return the value returned the first time.
You may also find this used when making database calls. For example, you may have a current_user method in a controller which returns the currently logged in user from the database. It’s highly likely you’ll want to call current_user
many times during your request and making a database query for each one is needlessly wasteful.
However, there are a couple of downsides to this technique. Firstly, it’s not always very clear how to clear the cache. Taking our Twitter example from above, let’s say we’ve got a user’s latest tweet but then we change their twitter_username attribute. The value returns by latest_tweet will still relate to the old username which may not be desirable.
Secondly, using ||=
will not cache any falsey values (specifically nil
and false
). It’s perfectly valid for our latest_tweet to return nothing if the user has no tweets but if we use ||=
it’ll never cache.
Finally, without manually creating multiple methods, there’s no simple way to call the latest_tweet
method but with ignoring the cache.
Introducing Memist
There are a number of Ruby libraries which handle memoization but I couldn’t find any that handled cache invalidation in the way I desired. So, as I often do, I made my own. Let’s take a look at how we can re-work our Twitter example using Memist:
class User
def latest_tweet
TwitterAPI.latest_tweet_for(self.twitter_username)
end
memoize :latest_tweet, :uses => [:twitter_username
end
You’ll immediately notice that we no longer have any caching logic within our method and all that is provided underneath. The first argument to memoize is the method name which we want to cache. Next, we define an array of attributes which are used in the method and should cause the cache to be invalidated if they change.
A whole wealth of options are now available to you:
user = User.new(:twitter_username => 'adamcooke')
# The first time it's called, it'll take a few seconds to get the
# date and return the Tweet itself.
user.latest_tweet #=> Tweet
# Subsequent calls are instantly returned from the cache.
user.latest_tweet #=> Tweet
# They also are marked a memoized so you know they've come from the cache
user.latest_tweet.memoized? #=> true
# If you then change the username and re-call the method.
# The value will be downloaded again.
user.twitter_username = 'atechmedia'
user.latest_tweet #=> Different Tweet
# Finally, if you want to get the latest tweet and skip all
# caching you can.
user.latest_tweet_without_memoization
More details about the library (including install instructions) are on the GitHub repository. Do let me know how you get on with using it!