DEV Community

Cover image for Be careful when memoizing booleans!
Oliver
Oliver

Posted on • Edited on

Be careful when memoizing booleans!

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
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

However, in this example, @happy becomes false:

def happy?
  @happy || @happy = false
  # @happy == false
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (5)

Collapse
 
val_baca profile image
Valentin Baca

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?
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
codeandclay profile image
Oliver • Edited

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
Enter fullscreen mode Exit fullscreen mode

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

Collapse
 
val_baca profile image
Valentin Baca • Edited

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
Thread Thread
 
codeandclay profile image
Oliver

🤦 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
Collapse
 
jsrn profile image
James

Very useful, thanks!