Skip to main content
Published in Engineering Insights

When Systems Can't Agree on the Truth

When Systems Can't Agree on the Truth
Table of contents
  1. TL;DR
  2. Truthiness in Ruby
  3. Project Boundaries
  4. The Truth of Truthiness
  5. How Could We Avoid This?
    1. Carefully Considering the Destination
    2. Ensure Boolean Values in Predicate Methods
    3. Ensure Boolean Values in Tests
  6. Summary

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:

Ruby
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:

Ruby
"Hello World" if 1   # => "Hello World"
"Hello World" if nil # => nil

Now there might be some surprises if you come from other languages:

Ruby
"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:

Ruby
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:

Ruby
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?)  endendNow 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:

Ruby
"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:

Ruby
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:

Ruby
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:

Ruby
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:

Ruby
expect(contrived_thing).to be_foo

but instead:

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

Frequently asked questions

Which values are falsey in Ruby?

Only nil and false are falsey. Zero and an empty string are both truthy.

Why did the regular expression result cause a bug?

The =~ operator returns an integer or nil. A match at the start of the string returns zero, which is truthy in Ruby. In the example, the database converts that zero to false.

How can a predicate return a strict boolean?

The example puts the regular expression match in a conditional expression ending in ? true : false. That makes complicated_foo? return true or false rather than an integer or nil.

What test helps catch this mismatch?

Use expect(contrived_thing.foo?).to eq true instead of expect(contrived_thing).to be_foo. The explicit check tests for true rather than accepting any truthy result.

Stay In The Loop

Never miss an update. Get the latest blogs, webinars, and expert insights straight to your inbox from The Gnar.