In Progress
Unit 1, Lesson 1
In Progress

Stay Positive

Video transcript & code

Here's a bit of Ruby code I ran across on the thoughtbot blog the other day.

def average(collection)
  if collection.length.zero?
    nil
  else
    collection.inject(0.0, :+) / collection.length
  end
end

This method calculates the average of a collection of numbers.

require "./average"

average([2, 4, 6, 8]) # => 5.0

…unless the collection is empty, in which case it returns nil.

require "./average"

average([]) # => nil

This special-case return value for empty collections avoids a divide-by-zero error.

Here's how the method checks for an empty collection.

if collection.length.zero?
  nil
else
  # ...
end

It asks the collection's length if it is zero. Checking the value of the length attribute echoes the fact that the length attribute is then used a few lines lower down.

collection.inject(0.0, :+) / collection.length

And asking the value if it is zero makes it very clear that it's a divide-by-zero situation that we're looking out for.

collection.length.zero?

That said, we could get the same information with one fewer method calls, by directly asking the collection if it is empty.

collection.empty?

Even after this change, there's still a pattern in this method which I usually try to avoid. This code prioritizes the edge-case. The first half of the method is concerned with dealing with the possibility of empty inputs. It's only after getting all that out of the way that we move on to the real "meat" of the method.

One alternative would be to move the empty-check into a guard clause, with an early return.

def average(collection)
  return nil if collection.empty?
  collection.inject(0.0, :+) / collection.length
end

This still puts the edge case first, but at least this version gets it out of the way more quickly, and doesn't stuff the interesting bits into an else clause.

Another possiblity it to put the happy path first, and the edge case later. To do this, we have to take the original if/else statement and invert the condition.

def average(collection)
  if !collection.empty?
    collection.inject(0.0, :+) / collection.length
  else
    nil
  end
end

I like that this defers the empty case till after the happy path. What I don't like about it is the negation. Every time some logic is negated, it adds a little extra mental overhead to keep track of.

So how do we ask the same question in a positive sense instead of a negative sense? You might know the answer already. Or, you might already know the method to use, but not realize it yet.

You may be aware of the #any? predicate on Ruby enumerable objects. With #any?, we can ask if there are any elements which have a particular property.

For instance, we can ask if there are any even numbers in a collection.

[1, 3, 5, 8, 9].any?{|n| n.even?}
# => true

Or if there are any three-letter words.

["apple", "banana", "strawberry"].any?{|n| n.length == 3}
# => false

What you might not know is that #any? can be sent without a block. In this case, it checks to see if the collection has any elements at all.

[].any?                         # => false
["apple"].any?                  # => true

And there we have the answer to our question.

In order to keep our conditional logic positive, we can use the #any? predicate to check if the collection has anything in it.

def average(collection)
  if collection.any?
    collection.inject(0.0, :+) / collection.length
  else
    nil
  end
end

This version puts the interesting and hopefully typical case first. It reads well too: "if there's anything in the collection, then perform an average reduction on it".

Incidentally, we don't actually need the else clause anymore.

def average(collection)
  if collection.any?
    collection.inject(0.0, :+) / collection.length
  end
end

As you can see when we try it out, this code gives us the exact same behavior as before.

require "./average3"
average([2, 4, 6, 8])           # => 5.0
average([])                     # => nil

This works because in Ruby, when an if statement's condition is not satisfied and it has no else clause, the default result value of the expression is nil.

if 2 == 3
  "nope"
end
# => nil

So there you have it. By striving to put the happy path first, and to avoid negative logic, we've both shortened and simplified this method. Staying positive doesn't just improve our lives, it improves our code as well! Happy hacking!

Responses