a journal
28 January, 2015
I wrote some quick thoughts on predicate methods a couple of weeks back, and received this comment from @fxn on twitter:
@relativesanity read the post, re "Ruby doesn't check that ? methods return booleans" ... in Ruby all values are booleans, that's why
— Xavier Noria (@fxn) January 14, 2015
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 puts
ing it? Let's check:
puts Report.get_results
#=> "Awesome results"
Wait what? How does this work?
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?
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.
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".
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.
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.