« HE:labs
HE:labs

Ruby Memoization - Part I

Postado por Flavia Fortes em 29/09/2014

Hello, guys! How have you been doing? :) Today I'll talk a little bit about memoization for ruby developers.

First of all, what's memoization? Well, memoization is a caching technique applied to optimize routines. Like, for example, when you store a returned value so next time you need to use it you avoid another call.

The simpler way to use memoization in ruby is by using the conditional assignment operator: ||=.

1 class User
2    # other methods
3   def facebook_friends
4     @facebook_friends ||= FacebookApi.get(:friends_list, self.facebook_id)
5   end
6 end

Note that when using facebook_friends you don't make uneeded calls to facebook api, because the result is memoized:

1 user.facebook_friends # => query the facebook api and return the friends
2 user.facebook_friends # => return the cached result

Basically, the code above means: @facebook_friends = @facebook_friends || FacebookApi.get(:friends_list, self.facebook_id).

OK, there are some fun controversies about this matter, some really smart guys say that the actual behavior is: @facebook_friends || @facebook_friends = FacebookApi.get(:friends_list, self.facebook_id).

For further information check this article, but let me show you why I think this is enjoyable:

Local variables

This is a classical example, using local variables:

1 > x = nil
2   => nil
3 > x ||= 1
4   => 1

On another irb session:

1 > x = nil
2  => nil
3 > x = x || 1
4  => 1
5 > x = x || 2
6  => 1    #just like we want

So far, it feels like x = x || 1, don't you think?

Hashes

Right, so we define a Hash like that:

1 > h = Hash.new(1)
2  => {}

The default value is what a Hash return without a defined key.

1 > h[:x]
2  => 1

But the Hash itself remains empty, as expected:

1 > h
2  => {}

And then:

1 > h[:x] ||= 2
2  => 1
3 > h
4  => {}

WOW, what just happened here? Wasn't h supposed to equal {:x=>1}?

And then we try this:

1 > h[:x] = h[:x] || 2
2  => 1
3 > h
4  => {:x=>1}

Right, different responses. So it doesn't feel like x ||= 1 behaves like x = x || 1 anymore.

If you're wondering, this doesn't happen if the Hash is set without a default value, like h = Hash.new. See for yourself:

1 > h = Hash.new
2  => {}
3 > h[:x]
4  => nil
5 > h[:x] ||= 1
6  => 1
7 > h
8  => {:x=>1}

Undefined variables

In these three examples below I used a new irb session, take a look:

1 > x ||= 1
2  => 1
1 > x || x = 1
2 NameError: undefined local variable or method `x' for main:Object
1 > x = x || 1
2  => 1

Annnnnnnd, are we back to the x = x || 1 behavior?

Observations

The main difference between x = x || 1 and x || x = 1 is essentially if you're assigning the value before or after the or comparison. So, yes, the second behavior makes more sense, but I couldn't find an explanation towards the undefined variables scenario. Something happens inside the ruby mechanisms. Anyway, feel free to add your opinions on our comments section. I'll be glad to hear your standpoints. :)

Plus

The boolean problem

Both nil and false are considered falsy values when found in boolean expressions, so look what happen when you use the conditional assignment operator with them:

1 > f = false
2  => false
3 > f ||= true
4  => true

Or, on a more daily example:

1 class User
2   # other methods
3   def has_credit?
4     @has_credit ||= begin
5       payload = BankAccountApi.get_balance(self.bank_account_id)
6       payload[:balance] > 0
7     end
8   end
9 end

If payload[:balance] > 0 returns false, the begin block will run again and again, making every api call until payload[:balance] > 0 returns true.

Whenever you need to memoize a method or variable that might return nil or false, avoid using the ||= idiom.

And that was what inspired this post, hahaha. Be very careful my friends! See you next time.

Compartilhe

Sabia que nosso blog agora está no Medium? Confira Aqui!