Singpolyma

Technical Blog

Why is That Value False?

Posted on

This post is a response to Boolean Externalities, so you should probably read that first.

The core of this problem is a lack of combinators. In order to propogate the reason why something is falsy, you need to add a lot of logic to each method, which noises up the code considerably.

So we start with a pseudo-falsy base class for all our possible “reasons”:

class WhyFalsy
	# This is so you can convert to bool with !!
	def !@
		true
	end

	def blank?
		true
	end
end

Now we can make objects that represent particular reasons, like:

class NoConfirmedAt < WhyFalsy; end

And use them in conditionals with only a little syntax pain:

!!NoConfirmedAt.new ? "confirmed at" : "not"

Now that we have these objects, we can create methods that let us combine them without throwing away the information we care about:

class WhyFalsy
	def or
		yield
	end

	def and
		self
	end
end

class Object
	def or
		self
	end

	def and
		yield
	end
end

class FalseClass
	def or
		yield
	end

	def and
		self
	end
end

class NilClass
	def or
		yield
	end

	def and
		self
	end
end

The normal && and || operators cannot be overloaded, so we define our own very similar methods, which allow us to write code like this:

def fully_authenticated?
	authenticated_accounts?.and { verified_email? }
end

Where any pseudo-falsy reason value we bubble up will be treated the same as the actual false or nil, and any other value will be treated truthily, same as before.

What if we want to wrap the result of some existing API (or DB column) in a reason? That’s a pretty easy smart constructor:

class WhyFalsy
	def self.wrap(x)
		# The !! makes fake-falsy values (like us) work
		!!x ? x : self.new
	end
end

Now we can easily wrap any existing API:

NotFree.wrap(free?)

Developers are lazy, and especially if they’re just using this in debugging they just want to use English text, like so:

class WhyFalsy
	attr_reader :msg

	def initialize(msg=nil)
		@msg = msg
	end
end

And while all these ways to know why something is falsy are nice, it would be even nicer if we could know *where* the falsy value came from:

class WhyFalsy
	attr_reader :msg, :backtrace

	def initialize(msg=nil)
		@msg = msg

		# Hack to get a backtrace
		begin
			raise
		rescue Exception
			@backtrace = $!.backtrace
			@backtrace.shift # Remove the reference to initialize
		end
	end
end

And there you have it, a complete, workable solution for propogating the information we care about all the way up to our call site.

Haskellers might recognize that this is basically a very-Rubyified encoding of the following:

import Control.Applicative
import Data.Monoid

instance (Monoid r) => (Either r) where
	empty = Left empty
	Left _ <|> x = x
	     x <|> _  = x

wrap True _    = Right ()
wrap False msg = Left msg

fully_authenticated = authenticated_accounts *> verified_email
available_to_user = free <|> (current_subscriber user)

The full code from this post is available as a gist.

Leave a Response