Read more

Careful with '||=' - it's not 'memoize'

Christoph Beck
January 05, 2014Software engineer

When you do something like this in your code:

def var_value
  @var ||= some_expensive_calculation
end
Illustration book lover

Growing Rails Applications in Practice

Check out our e-book. Learn to structure large Ruby on Rails codebases with the tools you already know and love.

  • Introduce design conventions for controllers and user-facing models
  • Create a system for growth
  • Build applications to last
Read more Show archive.org snapshot

Be aware that it will run some_expensive_calculation every time you call var_value if some_expensive_calculation returns nil.

This illustrates the problem:

def some_expensive_calculation
  puts "i am off shopping bits!"
  @some_expensive_calculations_result
end

When you set @some_expensive_calculations_result to nil, ||= runs some_expensive_calculation every time.

>   var_value
    i am off shopping bits!
    => nil 
>   var_value
    i am off shopping bits!
    => nil 
>   var_value
    i am off shopping bits!
    => nil

This only changes, when @some_expensive_calculations_result is something other than nil:

> @some_expensive_calculations_result = 42
    => 42 
>   var_value
    i am off shopping bits!
    => 42
>   var_value
    => 42 
>   var_value
    => 42 

So when you are in performance trouble if you do something like this:

def show
  @resource ||= User.find(resource_params(:id))
end

It will only cache only if a valid @resource is found. If the result is nil, the database is asked again and again.

Why?

'||=' is a shorthand vor "nil or undefined", so if you want to cache something that can return nil, you can not use it. You will have to check for 'defined?' only, ignoring 'nil?'

something like this would be correct:

def cache_or_resolve(instance_variable_name, resolver)
  if !instance_variable_defined? "@#{instance_variable_name}"
     instance_variable_set("@#{instance_variable_name}", send(resolver))
  end
  instance_variable_get("@#{instance_variable_name}")
end

def var_value
  cache_or_resolve :var, :some_expensive_calculation
end

This is what "memoize" in earlier rails versions did. There is a gem Show archive.org snapshot that reintroduces this behavior, but this is the essential implementation.

Remember

In the above example, some_expensive_calculation is never called again. This might be what you want, but if you are looking for something in the database that might return a valid result later on, you need to invalidate the cache manually:

def var_value_refreshed
  remove_instance_variable "@var"
  var_value
end

For the "propper" solution, see the gem mentioned. This only outlines the gun, the foot and the trigger.

Christoph Beck
January 05, 2014Software engineer
Posted by Christoph Beck to makandra dev (2014-01-05 18:53)