Relative Sanity

a journal

Known Unknowns

28 January, 2015

I wrote some quick thoughts on predicate methods a couple of weeks back, and received this comment from @fxn on twitter:

Hahaha, yes, very funny, except then you see code like this:

def output_results
  results = Report.get_results
  if results
    puts results
  else
    fail "no results"
  end
end

So Report.get_results sure looks like it returns a boolean, but then we're putsing it? Let's check:

puts Report.get_results
#=> "Awesome results"

Wait what? How does this work?

You can't handle the truth!

Ruby has a concept of values being "truthy" or "falsy". In essence, the joke is on us: everything in Ruby is "true" or "false".

Kinda.

Consider this:

puts "truthy" if "some string exists" #=> truthy
puts "truthy" if 1 #=> truthy
puts "truthy" if [:some, :array] #=> truthy
puts "falsy" unless nil #=> falsy

The pattern seems fairly clear, and actually quite useful. It means we can do clever things, such as our first example:

def output_results
  results = Report.get_results
  if results
    puts results
  else
    fail "no results"
  end
end

Breaking this down, the if condition evaluates the value of results for "truthiness". If the result of Report.get_results is nil, then results will be nil, hence falsy, hence we fall through to the failure.

Now, we're good cubscouts and we build out a test for our result printer. Months pass, and suddenly one day the test fails. Why?

One of these things is not like the others

Here's the rub: "truthy" means different things to different people:

puts "rut roh" if "" #=> rut roh

Empty Strings are "truthy". So are empty Arrays and empty Hashes. In fact:

puts "zomgwtfbbq" if 0 #=> zomgwtfbbq

Yup, 0 is truthy.

What's a coder to do? The key issue here is that we're inferring something from return values without explicitly asking a question. We expected get_results to return either something useful or nil, but it turns out someone changed that method to just return an empty string when there were no results. It seems reasonable to want a method to have a predictable return value, and there's nowt much more predictable than consistency. So we now live in an "all strings, all the time" world, and our client code lies broken.

We want better than "truthy": we want "useful".

If we're working in Rails-land, we get access to some helper methods: blank? and present?, which strive to do the right thing:

puts "woot" if "thing".present? #=> "woot"
puts "hell yeah" if "  ".blank? #=> "hell yeah"
puts "booyah" if [].blank? #=> "booyah"

But what if we're not depending on Rails? Well, Ruby provides #empty? on a few objects, but it's a little unpredictable:

puts "winning" if [].empty? #=> winning
puts "oh yeah!" if "".empty? #=> oh yeah!
puts "wait, what?" unless " ".empty? #=> wait, what?
puts "fuuuuuuuu" if nil.empty? #=> NoMethodError

empty? isn't universally applicable, meaning we still need to go and check the return values.

Useful is as useful does

Of course, the real need here is to define what "useful" means in the context of our application. Realising this, we might rewrite our code to make this definition explicit, instead of trying to shoe-horn it into the wide-reaching, generally applicable tools our language gives us. We might start with something like this:

def output_results
  results = Report.get_results
  fail "no results" unless useful? results
  puts results
end

def useful?(results)
  return false if results.nil?
  return false if results.empty?
  true
end

We can then store all our concepts of what makes a set of results "useful" in one place, for easy viewing and reasoning about. It might even be tempting to pull that definition up to the Report.get_results method in the first place, but that makes assumptions that what one part of our application finds "useful" is the same as what other parts will find "useful" too. What makes most sense depends heavily on the definitions of "useful", and to my mind it feels an easier change to centralise the definition in future than to have to split it out to all the client callers later.

As ever, "it depends".

Bang bang you're dead

Sometimes, of course, you really can just rely on the truthy or falsy values. Take this example of some customer data we get back from an API query:

ue_data = {
  :name => "Universal Exports",
  :priority => "yes"
}
sp_data = {
  :name => "SPECTRE"
}

We want to wrap this data up in an object we can use. Our first pass is just to use a simple wrapper object:

class Customer < Struct.new(:data)
  def priority
    data[:priority]
  end
  def name
    data[:name]
  end
end

ue = Customer.new(ue_data)
sp = Customer.new(sp_data)
[us, sp].each do |customer|
  puts "#{customer.name} rocks" if customer.priority
end #=> "Universal Exports rocks"

That's all good, but we're back to implicit return value dependencies. priority represents a boolean concept, so it would be good if our Customer object could reflect that, even though the data structure does not.

Ruby idiomatically provides a way to do that. The negation operator ! will always return a boolean, and accepts truthy operands. In other words, !data[:priority] will almost do exactly what we want: it converts the value into a boolean which exactly (if oppositely) matches the truthy or falsy evaluation.

If only we could invert the result, we'd be home freeā€¦

def priority
  !! data[:priority]
end

This does precisely what we're looking for: takes a truthy or falsy value, converts it into an (inverted) Boolean representation, and then inverts it back to where we were. To be even more idiomatic:

def priority
  data[:priority]
end
def priority?
  !! priority
end

and now we have access to both the actual data value and a way of checking it as a flag.

Return to sender

Ruby is pretty fast and loose with the concept of return values: it will often "just work" if you don't really think about them at all. However, as with much programming magic, relying on this for a long enough time means that some day, things will break.

If you're asking a question, try to use (or create) a predicate method. It's the easiest way to be sure that six months from now, the answers won't have change.