by Mark Lodato
- Be careful about
truevs truthy on the boundaries of your application
- Consider returning
falseexclusively in predicate methods
- Consider testing
.to eq trueinstead of
Truthiness in Ruby
Ruby, being duck-typed, does not strictly enforce "boolean" values:
if value do_this else do_that end
In this case, we don't care if
a complex object. Ruby has a method for determining if any kind of value is
"truthy" or "falsey". For Ruby, the rules are fairly lean:
- Everything else is truthy
"Hello World" if 1 # => "Hello World" "Hello World" if nil # => nil
Now there might be some surprises if you come from other languages:
"Hello World" if "" # => "Hello World" "Hello World" if 0 # => "Hello World"
Those wouldn't fly in all languages, but that's fine...unless...
Let's contrive a thing to make a point:
class ContrivedThing < ApplicationRecord def self.foo where(id: all.select(&:foo?)) end def foo? complicated_foo? end private def complicated_foo? # ... do complex work ... complicated_derived_thing =~ /fancy regex/ end end
And we can test it like:
But there could be a problem! What if we have lots of
all.select(&:foo?) loads all the records into memory,
does a complicated calculation on each to
determine which are
foo?-ey, and then wraps them in a huge binds list
for query chaining.
As much as possible, we'd like to keep this in SQL so we decide to memoize
the value of
foo? in a boolean column named
foo in the database:
class ContrivedThing < ApplicationRecord before_save :memoize_foo private def memoize_foo return unless certain_things_changed? assign_attributes(foo: complicated_foo?) end end
Now we can delete
ActiveRecord will create scopes and predicate methods for
boolean columns) and we've vastly improved the performance of the scope.
But there's a catch...
The Truth of Truthiness
We've been using
foo? as a truthy/falsey identifier but what is it actually?
Well its return value comes from the
=~ operator which
returns integer or nil.
So we might have:
"string" =~ /string/ # => 0
which is properly truthy in Ruby because there was a match. Unfortunately,
we're directly serializing that value to our postgresql database which
coerces zero to false.
Now we have a bunch of values that used to be truthy in Ruby but now are falsey
because postgres converted them to a strict
How Could We Avoid This?
Carefully Considering the Destination
One approach involves
converting the value
before we head off to the database:
def memoize_foo return unless certain_things_changed? boolean_value = ActiveModel::Type::Boolean.new.cast(complicated_foo?) # Rails 4.2 - ActiveRecord::Type::Boolean.new.type_cast_from_database() # Rails 4.1 and below - ActiveRecord::ConnectionAdapters::Column.value_to_boolean assign_attributes(foo: boolean_value) end
Which works wonderfully, but can be forgotten and is only really caught
if you happened to write a test like:
thing = ContrivedThing.new() # setup to allow complicated_foo? to return 0 thing.save expect(thing).to be_foo
Ensure Boolean Values in Predicate Methods
predicate methods (those whose name ends with a question mark) return
So one way around our bug is:
def complicated_foo? complicated_derived_thing =~ /fancy regex/ ? true : false # => true/false end
Ensure Boolean Values in Tests
Following the above option, what if our original test wasn't:
expect(contrived_thing.foo?).to eq true
This sort of testing isn't nearly as pretty, but helps prevent these integration
- Ruby's handling of predicate methods, predicate matchers, and truthy/falsey
values allows for light, expressive, pleasing code, but can result in subtle
bugs when interacting with outside sources.
- Be extra careful at application boundary points that there are no assumptions
about how languages interpret different values.
- When in doubt, be overly specific to ensure values can't be misunderstood.
Learn more about how The Gnar builds Ruby on Rails applications.