In Progress
Unit 1, Lesson 1
In Progress

Predicate Return Value Part 1

Video transcript & code

Programmers argue about the darnedest things sometimes. One of the longest Github debate threads I've ever read centered around what kinds of value a predicate method should return.

Predicate methods, as you probably know, are query methods that answer a yes-or-no question. In Ruby, we normally name predicates with a question mark on the end. For instance, if we ask the number 2 if it is even, it returns true. If we ask the number 3 the same question, it answers false.

2.even?                         # => true
3.even?                         # => false

It may seem strange that there would be any question at all about what these kinds of methods should return. I mean, they return either true or false, right? Not a lot of room for debate there.

But, as is so often the case, it's not quite that simple. For instance, consider this method. It is supposed to tell someone that their coffee is ready. If their coffee has any sweetener, it says "here's your sweetened coffee". If not, it just says "here's your coffee".

def announce_coffee(coffee)
  if coffee.sweetened?
    puts "Here's your sweetened coffee"
  else
    puts "Here's your coffee"
  end
end

Now, here's a coffee class that could work with the method. It has a sweetener attribute. It also has a sweetened? predicate. If the coffee has some kind of sweetener in it, it returns true. If the coffee has had no sweetener, it returns false.

class Coffee
  attr_accessor :sweetener

  def sweetened?
    if sweetener
      true
    else
      false
    end
  end
end

Let's try this out. We start with black coffee. When we ask it if it is sweetened, it says no. Then we add some cane sugar. This time, it tells us it is sweetened.

require "./coffee"

c = Coffee.new
c.sweetened?                    # => false
c.sweetener = "cane sugar"
c.sweetened?                    # => true

This seems like a long method for such a trivial task. How could we shrink it down?

This being Ruby, there are a lot of ways we could incrementally tighten up this code. I'm going to skip right to one of the simplest possible forms: instead of a conditional, we simply return the value of the sweetener attribute.

class Coffee
  attr_accessor :sweetener

  def sweetened?
    sweetener
  end
end

But wait, does this even do the right thing anymore? Well, let's take a look. If we take black coffee, and ask it if it is sweet, it returns nil, because the sweetener attribute has not been set. As I'm sure you know, there are two "falsy" values in Ruby: one is false, and the other is nil.

When we add some sweetener, and then ask again, the result is the new value of the sweetener attribute. This value is neither false, nor nil. All other values in Ruby are considered "truthy", so this is a truthy return value.

require "./coffee2"

c = Coffee.new
c.sweetened?                    # => nil
c.sweetener = "stevia"
c.sweetened?                    # => "stevia"

So while this predicate no longer returns literal true and false values, it still returns values that are truthy or falsy depending on whether a sweetener is present. Is this good enough? Well, let's run it through our announcer method. Passing in black coffee, then passing in sweetened coffee, we see that it is announced correctly both times. These truthy and falsy values are good enough to satisfy the conditional code.

require "./announce"
require "./coffee2"

c = Coffee.new
announce_coffee(c)
c.sweetener = "agave nectar"
announce_coffee(c)

# >> Here's your coffee
# >> Here's your sweetened coffee

This is an important discovery, because it means that we can write predicate methods very simply. In fact, we could go one step further with our predicate method. Instead of defining a method, we could simply define the predicate as an alias for the sweetener reader attribute.

class Coffee
  attr_accessor :sweetener
  alias_method :sweetened?, :sweetener
end
require "./coffee3"

c = Coffee.new
c.sweetened?                    # => nil
c.sweetener = "stevia"
c.sweetened?                    # => "stevia"

If you think it's bizarre for a predicate method to return something other than true or false, you should know that Ruby itself does the same thing. Here's one example: The #nonzero? predicate method on numbers returns either nil or the number itself. So for instance, when sent to 42 it returns 42. When applied to 0 it returns nil.

42.nonzero?                     # => 42
0.nonzero?                      # => nil

The Ruby documentation points out that this is useful for chaining comparisons. Here's an example straight from the docs:

a = %w( z Bb bB bb BB a aA Aa AA A )
b = a.sort {|a,b| (a.downcase <=> b.downcase).nonzero? || a <=> b }
b   # => ["A", "a", "AA", "Aa", "aA", "BB", "Bb", "bB", "bb", "z"]

The #sort method expects its block to return -1, 0, or 1 for each comparison. This code compares downcased versions of each string. Only when the downcased versions compare as equal does it switch to comparing the strings directly. In this way it keeps letters together, and only factors in capitalization within the same letter group.

And it's not alone. Another example is the File.size? predicate, which returns nil for files that don't exist or have zero size, and an integer otherwise.

File.size?("NONESUCH")          # => nil
File.size?("294-predicate-return-value.org") # => 13178

So not only is it possible to write a working predicate method that returns values other than literal true and false, examples of these methods can be found in the Ruby core libraries.

The next question is whether predicates that behave this way are a good idea. That turns out to be a topic with a lot of subtleties to it, and if I covered them all today, this episode would run far overtime.

In the next episode we'll look at some of arguments against these kinds of predicate methods, and we'll try to formulate some guidelines for working with predicates. Today I'm just going to leave you to think about the possible implications of non-boolean-returning predicates. If you want, you can also try to find some other core Ruby methods which have this type of behavior. I'll give you one hint: File.size? isn't the only File method to act like this.

Until part two, happy hacking!

Responses