Back to Home

 What is Ruby's ||= Really Does

Feb 1, 2017

A common misconception is that a ||= b is equivalent to a = a ||= b, but it behaves like a || a = b

In a = a || b, a is set to something by the statement on every run, whereas with a || a = b, a is only set if a is logically false (i.e. if it's nil or false) because || is 'short circuiting'. That is if the left hand side of the || comparison is true, there is no need to check the right hand side.

case1. When 'a' is nil or false a = nil b = 7 a ||= b a => 7 b => 7
case2. When 'a' has a truethy value a = 17 b = 19 a ||= b a => 17 b => 19

Simple Memoization with ||=

Memoizataion is a technique you can use to speed up your accessor methods. It caches the results of methods that do time-consuming work, work that only needs to be done onece. In Rails, you see memoization used so often that it even included a module that would memoize methods for you. We will see that later.

class User < ActiveRecord::Base def twitter_followers @twitter_followers ||= twitter_user.followers end end

The twitter_followers interpreted to:

@twitter_followers = @twitter_followers || twitter_user.followers. That means that you'll only make the network call the first time you call twitter_followers, and future calls will just return the value of the instance variable @twitter_followers

Multi-line memoization

class User < ActiveRecord::Base def main_address @main_address ||= begin maybe_main_address = home_address if prefers_home_address? maybe_main_address = work_address unless maybe_main_address maybe_main_address = address.first unless maybe_main_address end end end

The begin..end creates a block of code in Ruby that can be treated as a single thing. That's why ||= works just as well here as it did before.

What about nil?

The problem is that when right side of the expression returns nil, it will perform the expensive fetches again. So instead of using ||=, consider if / else statement.

class User < ActiveRecord::Base def twitter_followers return @twitter_followers if defined? @twitter_followers @twitter_followers = twitter_user.followers end end

In order to understand the above example, let's talk about 'defined?'. 'defined?' is Conditional Execution. defined? operator returns nil if its argument (which can be an arbitrary expression) is not defined, otherwise it returns a description of that argument. If the argument is yield, defined? returns the string 'yield' if a code block is associated with the current context. Here are the examples.

defined? 1 => "expression" defined? String => 'constant' definedd? c = 1 => 'assignment' a = 1 defined? a => 'local_variable' b = nil defined? b => 'local_variable' defined? dd => nil dd = 3 if defined? dd => 3

So go back to the our exmample of @twitter_followers, if it returns nil, instead of it will run and try to set @twitter_followers, it will be just nil.

What about parameters?

We have some memoization patterns that work well for simple accessors. But what if you want to memoize a method that takes parameters?

#app/models/city.rb class City < ActiveRecord::Base def self.top_cities(order_by) where(top_city: true).order(order_by).to_a end end

It turns out that Ruby's Hash has an initalizer that works perfectly for this situation. Before going into this, let's look at what new { |hash, key| block } statement.

The block passed after the new keyword is pattern to create default value. When you pass block after Hash.new, it will creates a new default object each time. For instance

h = Hash.new {|hash, key| hash[key] = 'Go Fish: #{key}'} h['c'] => 'Go Fish: c' h['d'].update! => 'GO FISH: D' h.keys => ['c', 'd']

Now, look at this example

class City < ActiveRecord::Base def self.top_cities(order_by) @top_cities ||= Hash.new do |hash, key| h[key] = where(top_city: true).order(key).to_a end @top_cities[order_by] end end

No matter what you pass into order_by, the correct result will get memoized.Since the block is only called when the key doesn't exist, you don't have to worry about the result of the block being nil or false.