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
Top comments (5)
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.)"
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 tonil
.This is what I get in
pry
: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 statementa = 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 ifa
doesn't change, thennil
will be printed.🤦 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:
Very useful, thanks!