Memoization in Ruby often looks harmless:
def active?
@active ||= compute_active_flag
end
It’s clean. It’s idiomatic. It avoids duplicate work.
For years, this pattern feels safe.
But ||= does not mean “if unset.”
It means “if falsey.”
And in long-lived systems, that difference matters.
The subtle behavior
This:
@active ||= compute_active_flag
means Ruby assigns only when @active is nil or false.
So, since nil and false are the only falsey values in Ruby:
-
@activeisnil-> compute -
@activeisfalse-> compute -
@activeis truthy -> reuse
Memoization with ||= is only reliable when the memoized value is guaranteed to become truthy.
How this causes confusion over time
In one system I worked on, a boolean flag was memoized exactly like this.
At first, the flag was effectively “unset or true,” and everything worked.
Later, the domain evolved. false became an explicit, meaningful result based on new business rules.
That’s when it got strange:
- the method recomputed unexpectedly, silently destroying performance if
compute_active_flagwas expensive - tests became noisy and inconsistent
- code readers assumed memoization was happening, because the syntax looked memoized
The code looked correct.
The semantics weren’t.
That gap is where long-lived systems accumulate friction.
Why this happens
As systems mature:
- flags gain new states
- defaults become explicit
- “maybe missing” becomes “intentionally false” (or sometimes intentionally
nil)
But ||= never changes its behavior. It keeps encoding a truthiness assumption that may no longer fit your domain.
This isn’t a Ruby flaw.
It’s a reminder that convenience operators carry semantics.
Safer patterns
If false is valid, but nil still means “unset”:
def active?
return @active unless @active.nil?
@active = compute_active_flag
end
If both false and nil are legitimate cached values, guard on definition instead using defined? (which is slightly faster, as it's evaluated by the parser rather than as a method call) or instance_variable_defined?:
def active?
return @active if defined?(@active)
@active = compute_active_flag
end
Now false and nil are preserved as cached results, and behavior matches intent.
It’s a few more characters.
It’s also semantically honest.
The broader lesson
Most issues in long-running systems aren’t dramatic failures.
They’re small mismatches between yesterday’s assumptions and today’s domain.
||= is a perfect example: convenient, idiomatic, and excellent in the right context.
But when false or nil becomes meaningful, tiny semantics compound.
Explicitness costs a little now.
It usually saves much more later.
Top comments (0)