This post is a response to
, 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)
|>|>|>