loading...
Cover image for Be careful when memoizing booleans!

Be careful when memoizing booleans!

updated_tos profile image Null Updated on ・3 min read

It took me an hour or so of frustration to figure out why a method was being called multiple times despite my attempt at memoizing its return value.

The problem

My problem looked a bit like this.

def happy?
  @happy ||= post_complete?
end

My intention was that value of post_complete? would be stored as @happy so that post_complete? would be fired only once.

However, that's not guaranteed to happen here. post_complete? might be evaluated and its value assigned to @happy every time I call happy?.

Can you see why?

  @happy ||= post_complete?

What's going on?

The question mark denotes that post_complete? is expected to return a boolean value. But, what if that value is always false?

Another way of writing the statement is:

@happy || @happy = post_complete?

In the above example, I want to know if at least one of the sides is true.

Remember that if the left-hand side of an || statement is false, then the right-hand side is evaluated. If the left side is truthy, there's no need to evaluate the right side – the statement has already been proven to be true – and so the statement short circuits.

If I replace post_complete? with boolean values, it's easier to see what is happening.

In this example, @happy becomes true:

def happy?
  @happy || @happy = true
  # @happy == true
end

However, in this example, @happy becomes false:

def happy?
  @happy || @happy = false
  # @happy == false
end

In the former, @happy is falsey the first time the method is called, then true on subsequent calls. In that example, the right-hand side is evaluated once only. In the latter, @happy is always false and so both sides are always evaluated.

When using the ||= style of memoization, only truthy values will be memoized.

So the problem is that if post_complete? returns false the first time happy? is called, it will be evaluated until it returns true.

A solution

So how do I go about memoizing a false value?

Instead of testing the truthiness of @happy, I could check whether or not it has a value assigned to it. If it has, I can return @happy. It if hasn't, then I will assign one. I will use Object#defined?.

The documentation states:

defined? expression tests whether or not expression refers to anything recognizable (literal object, local variable that has been initialized, method name visible from the current scope, etc.). The return value is nil if the expression cannot be resolved. Otherwise, the return value provides information about the expression.

Note that the expression is not executed.

I use it like so:

def happy?
  return @happy if defined? @happy
  @happy = false
end

Referring back to the documentation, there's one thing I need to be aware of. This isn't the same as checking for nil or false. It's a bit counterintuitive, but defined? doesn't return a boolean. Instead, it returns information about the argument object in the form of a string:

> @a, $a, a = 1,2,3
> defined? @a
#=> "instance-variable"
> defined? $a
#=> "global-variable"
> defined? a
#=> "local-variable"
> defined? puts
#=> "method"

If I assign nil to a variable, what do you think the return value will be when I call defined? with that variable?

> defined? @b
#=> nil
> @b = nil
#=> nil
> defined? @b
#=> "instance-variable"

So, as long as the variable has been assigned with something (even nil), then defined? will be truthy. Only if the variable is uninitialized, it returns nil.

Of course, you can guess what happens when we set the variable's value to false.

> @c = false
#=> false
> defined? @c
=> "instance-variable"

Update: An improved solution

Prompted by Valentin Baca's comment, I've reassessed my original solution. Do I really need to check whether or not the variable is initialised or is checking for nil enough?

@happy.nil? should suffice as I'm only interested in knowing that the variable is nil rather than false. (false and nil are the only falsey values in Ruby.)

I think this version is more readable:

def happy?
  @happy = post_complete? if @happy.nil?
  @happy
end

Wrapping up

I now know that the ||= style of memoization utilizes short-circuiting. If the left-hand side variable is false, then the right-hand part of the statement will be evaluated. If that's an expensive method call which always returns false, then the performance of my program would be impacted. So instead of ||= I can check if the variable is initialized or I can check if it's nil.

And now I'm happy.

def happy?
  @happy = post_complete? if @happy.nil?
  @happy
end

Posted on by:

updated_tos profile

Null

@updated_tos

Junior developer experienced in Ruby. Looking for remote work or work in or around Somerset, UK!

Discussion

pic
Editor guide
 

c/o github.com/rubocop-hq/ruby-style-g...

"Don't use ||= to initialize boolean variables. (Consider what would happen if the current value happened to be false.)"

# bad - would set enabled to true even if it was false
enabled ||= true

# good
enabled = true if enabled.nil?

I've found that when using a language, style guides are more valuable than just style; they can help me avoid common pitfalls like this.

 

Thanks. I'd never considered styleguides to be such an invaluable resource.

@happy = post_complete? if @happy.nil? is a lot more succinct.

I'm going to update the post as I actually prefer this and I think it should be mentioned.

It may be that I'm tired and haven't understood it properly but as I understand it, it doesn't do exactly the same thing.

The first time, it evaluates to false. The second time, however, it evaluates to nil.

This is what I get in pry:

> a = false if a.nil?
#=> false
> a = false if a.nil?
#=> nil

I'll need to take a look again at this in the morning. Any input would be welcome!

 

You're confusing the value of a with what the statement a = false if a.nil? is returning.

The console prints the output value of the whole statement; not necessarily the value of a.

If the value of a did change (as in, when it was assigned to), then that new value will print in the console. But if a doesn't change, then nil will be printed.

irb(main):001:0> a = false if a.nil?
=> false # this false is what "a = false if a.nil?" returned. It's false because 'a' was set to 'false'.
irb(main):002:0> a
=> false
irb(main):003:0> a = false if a.nil?
=> nil # this nil is what "a = false if a.nil?" returned. It's nil because 'a' was NOT set to anything.
irb(main):004:0> a
=> false

🤦 Of course. Thanks for the explanation. I'd convinced myself that it was returning the variable's value. Doh!

So, in my case I would use it like so:

  def happy?
    @happy = post_complete? if @happy.nil?
    @happy
  end
 

Very useful, thanks!

 

This is really good to know! And also a really good, simple explanation of memoization in general.