Ruby - Memoization

Subscribe to receive new articles. No spam. Quality content.

Hi. Today I would like to talk about memoization in Ruby.

But to begin, let's consider some abilities of Ruby. Often, we want to assign result of execution of function to some variable. If result of execution is false or nil we would like to assign some default value.

Let's show possible, but ugly code:

if @page.title
  site_title = @page.title
else
  site_title = "Blog about Ruby and Ruby On Rails"
end

In this case @page.title can be nil if user didn't fill in page title. If title of page is not nil - we use it. If it's nil - we return default value.

Now let's check how can we implement that logic without if..else:

site_title = @page.title || "Blog about Ruby and Ruby On Rails"

If describe that code by words, that would be something like that: if @page.title is not nil or false, assign it's value to site_title, else use default value.

Let's remember that idea with || and move forward to memoization.

Here is how wiki describes memoization:

In computing, memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

Main idea to make sure that when function is being called it caches results and re-uses it on next calls, avoiding heavy calculations.

Let's consider code which isn't using memoization. In ApplicationController we added method current_user, which other controllers of our app use to get logged-in user:

class ApplicationController < ActionController::Base
  def current_user
    User.find(session[:user_id])
  end
end

This method fetches data every time (!) when we call current_user method, sending query to DB. If we add memoization we will fetch user data only once (on first call of current_user).

Adding memoization is as simple as using || which we discussed at the beginning of this article:

class ApplicationController < ActionController::Base
  def current_user
    @current_user ||= User.find(session[:user_id])
  end
end

Again, if we describe by words that code, we will get the following: if class variable @current_user equals to false or nil - try to find user by session param and assign it to @current_user. If user already fetched and variable @current_user is set, just return that value.

So memoization in Ruby can be done as easy as using ||=.

If you want to add memoization to method which is more than one line of code - it's ok, you can use begin..end for that:

def roles_with_users_count
  @roles ||= begin
    Role.all.inject({}) do |hash, role|
       hash.update(role.name => User.with_role(role).count)
    end
  end
end

Code inside begin..end is not that important. The main part here is the idea of using begin..end for memoization.

Use memoization: if you do heavy calculations, or fetching data from database and it's enough for you to do that once. It's just some sort of caching.

Don't use memoization in methods, which accepts params. In this case results of calculation will change depending of accepted param. But even for such cases there are options.

Don't use memoization in methods which operates by instance variables as well:

# don't do that
def full_name
  @full_name ||= "#{@first_name} #{@last_name}"
end

@first_name = 'Sergii'
@last_name = 'M'

puts full_name # => "Sergii M"

@first_name = 'John'
@last_name = 'Doe'

puts full_name # => "Sergii M"

Variables changed, but we already cached initial value into @full_name. And return it every time when we call full_name without any connection to current state of @first_name and @last_name.

Good luck in using of memoization in your projects!

PS: if some examples are hard to understand, it's good idea to test everything in irb on some simpler examples:

a = nil || "foo" # => "foo"
b = false || "bar" # => "bar"
c = "foo" || "bar" # => "foo"

Or this example:

def get_number
  puts "getting number"
  10
end

def memoized_number
  @number ||= get_number
end

On first call of memoized_number it calls get_number and we will see "getting number" and 10 will be assigned to @number. On second call of memoized_number we will not see "getting number", because it will not call get_number it will return cached value of 10.

Thanks for reading!

Subscribe to receive new articles. No spam. Quality content.

Comments