When Systems Can’t Agree on the Truth

  • June 26, 2019
  • Pete Whiting
  • 4 min read

by Mark Lodato

TL;DR

  • Be careful about true vs truthy on the boundaries of your application
  • Consider returning true and false exclusively in predicate methods
  • Consider testing .to eq true instead of .to be_truthy in RSpec

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 value is true or false or 1 or 0 or
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:

  • nil is falsey
  • false is falsey
  • Everything else is truthy

Therefore:

"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...

Project Boundaries

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:

expect(contrived_thing).to be_foo

But there could be a problem! What if we have lots of ContrivedThings?
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 ContrivedThing::foo and ContrivedThing#foo? (because
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 false value.

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

There's clearly some heat around
this topic in general but the
official Ruby FAQ recommends:

predicate methods (those whose name ends with a question mark) return true or false.

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).to be_foo

but instead:

expect(contrived_thing.foo?).to eq true

This sort of testing isn't nearly as pretty, but helps prevent these integration
pains.

Summary

  • 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.

Interested in building with us?