Advanced Memoization in Ruby

memoization, performance, rails, ruby

The last post on the basics of memoization covered what memoization was, how you can use it in your application, and some of the potential pitfalls.

In this post we’ll look at advanced assignment techniques, we’ll fix where basic memoization can fail, and you’ll see how to perform a more advanced memoization.


To refresh your memory, here’s a really basic example of memoization using the conditional assignment operator:

1
2
3
def current_user
  @current_user ||= User.find(session[:user_id])
end

Most times you can use a straight up single statement for performing memoization. But as your application grows you’re going to need additional, more expressive ways to perform memoization and build complex objects.

Advanced Assignment

Because Ruby is so awesome you can do memoization assignment with an if/else statement:

1
2
3
4
5
@current_user ||= if session[:user_id]
                    User.find(session[:user_id])
                  else
                    User.new(guest: true)
                  end

It came as a real surprise to me when I found out that you can assign the results of an if/else to a variable. Another surprise was being able to use begin and end, like this:

1
2
3
@params ||= begin
              # do work
            end

These are two ways you can write a more advanced memoization method.

Where Conditional Assignment Fails

If you’re not careful, conditional assignment can bite you in the butt! Try this code in irb:

1
2
3
4
5
6
7
8
9
10
11
12
13
def foo
  @foo ||= begin
            puts "hit"
             sleep 5
             false
           end
end

foo()
# => "hit"

foo()
# => "hit"

Surprised that "hit" was printed twice? It turns out the conditional assignment operator is trickier than you thought!

Conditional assignment is always going to run if the variable @foo is falsely — that is if the value of @foo is nil or false.

Now it’s fairly rare to have code return false in a memoization scenario, but in the event that your code does, you can get around it using if_defined? and a guard return:

1
2
3
4
5
6
7
8
9
10
11
12
def foo
  return @foo if defined?(@foo)

  puts "hit"
  sleep 5
  @foo = false
end

foo()
# => "hit"

foo()

This is a safer pattern to use when writing your memoization code, but more verbose versus the idiomatic Ruby version described previously. My personal preference is to use this only when it’s needed.

Memoizing with Parameters

In the last post on memoization one of the places I recommended not to use memoization was with parameterized methods. That was partially true because conditional assignment doesn’t handle parameters. However, you can get around that limitation by using a hash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A
  def initialize
    @results = {}
  end

  def expensive_operation(p1)
    return @results[p1] unless @results[p1].nil?

    @results[p1] = begin
                     puts "hit"
                     sleep 5
                   end
  end
end

a = A.new
a.expensive_operation('a')
# => "hit"

a.expensive_operation('a')

At this point you’re effectively re-implementing a cache. And to be perfectly honest, there are people smarter than you and I that have worked long and hard on cache, so there’s no point reinventing the wheel there! Cache on the other hand, is a future topic to dive into!

There is one final thing if you’re really looking to use a memoize technique verses cache, and that is to use the memoizable gem. Originally this code was within Rails itself, but was pulled out and moved into it’s own seperate gem. It’s overkill for something like a current_user memoization but works great to keep you from implementing the code above!

That wraps up the post on advanced memoization. While this post focused on memoization techniques that are applicable to Ruby (and Rails), the primary focus of this blog is improving Rails performance. If you want to receive regular tips on improving Rails performance add your name in the below box:

I will not sell your email address, you can unsubscribe at anytime, and I am not an affiliate for anything. In the event that I build or make something related to this blog, I'll send you discounts, early access, and freebies!

This page was delicately crafted on by .