In Progress
Unit 1, Lesson 1
In Progress

# Spaceship Revisited

Video transcript & code

Back in Episode #205, we met the "spaceship operator". The spaceship, as you may recall, is an amalgam of the lesser-than, equals, and greater-than methods all rolled into one. Depending on the relationship of the left hand to right-hand operands, it will return -1, 0, or 1.

``````1 <=> 2                         # => -1
2 <=> 2                         # => 0
3 <=> 2                         # => 1
``````

But that's not the whole story. What I failed to show in that episode is what happens when the two values are not compatible. Let's try comparing a string to an integer. What do you expect to happen? We might expect this to raise a `TypeError`. Instead, what we get is `nil`.

``````"foo" <=> 123                   # => nil
``````

In Episode #205, we discovered how the spaceship operator was important because it makes the `Comparable` module possible. Here's a simple example. We have a beer class, which has a `brewer` attribute.

It implements the spaceship operator in terms of the `brewer` attribute, in such a way as to indicate that some beers are greater than others.

Having given the class a spaceship operator, we then include `Comparable`.

``````class Beer
include Comparable

def initialize(brewer)
@brewer = brewer
end

def <=>(other)
case other.brewer
when self.brewer then 0
when "Victory" then -1
else 1
end
end
end
``````

We can now compare one beer to another to see if they are equal, even though we didn't explicitly define the equality operator. We can also check if one beer is greater than another.

``````require "./beer"

Beer.new("Victory") == Beer.new("Budweiser")
# => false

Beer.new("Victory") == Beer.new("Victory") # => true

Beer.new("Victory") > Beer.new("Budweiser") # => true
``````

This is fine for comparing beers. But what happens when we accidentally compare a beer to a non-beer object, say, a string? In this case, we get an error. This is because we wrote the spaceship operator in such a away that it expects its argument to respond to the `brewer` message.

``````require "./beer"

Beer.new("Victory") <=> "Victory"
# =>
# ~> /home/avdi/Dropbox/rubytapas/218-spaceship-revisited/beer.rb:11:in `<=>': undefined method `brewer' for "Victory":String (NoMethodError)
# ~>    from -:3:in `<main>'
``````

We might expect this exception to crop up in the derived `Comparable` operators as well. But when we do an equality comparison between a `Beer` a and a string, we just get `false`. This is because, as we discussed in the other episode, `Comparable` currently eats exceptions. However, we can't rely on this behavior: it is deprecated and will be removed in a future version of Ruby.

``````require "./beer"

Beer.new("Victory") == "Victory"
# => false
``````

Let's look at another application of the spaceship operator: sorting. When we put a beer and a non-beer in an array together and try to sort it, we get our exception again.

``````require "./beer"

[Beer.new("Victory"), "Victory"].sort
# =>
# ~> /home/avdi/Dropbox/rubytapas/218-spaceship-revisited/beer.rb:11:in `<=>': undefined method `brewer' for "Victory":String (NoMethodError)
# ~>    from -:3:in `sort'
# ~>    from -:3:in `<main>'
``````

Let's summarize what we've found so far:

First, our spaceship operator may raise a `NoMethodError`, which differs from the behavior of Ruby's built-in objects. Ruby core objects return nil when they are compared to incompatible objects.

Second, in some future version of Ruby this will also cause the operators provided by `Comparable` to signal an exception as well. This is a problem, since we normally don't expect comparison operators to raise exceptions.

Third, when we try to sort `Beer` objects, we get an error which might be helpful to us, but which is likely to be unintelligible to anyone else who uses the class.

Let's update our spaceship definition to behave more like Ruby's built-in implementations. We'll add a guard clause at the top which checks that the other object is compatible. If not, it short-circuits and returns `nil`.

``````class Beer
include Comparable

def initialize(brewer)
@brewer = brewer
end

def <=>(other)
return nil unless other.is_a?(self.class)
case other.brewer
when self.brewer then 0
when "Victory" then -1
else 1
end
end
end
``````

Let's try our comparisons again. Comparing a Beer to a string results in a `nil` value. Doing an equality comparison still results in `false`.

When we re-try our `sort`, we see something interesting: a new error message saying "comparison of Beer with String failed". What happened here is that Ruby's sort understood the `nil` return to mean that the types were incompatible. And it has a special error message for just that scenario. By complying with Ruby's conventions for spaceship return values, we got a more helpful error message in return.

``````require "./beer2"

Beer.new("Victory") <=> "Victory" # => nil
Beer.new("Victory") == "Victory"  # => false
[Beer.new("Victory"), "Victory"].sort
# =>
# ~> -:5:in `sort': comparison of Beer with String failed (ArgumentError)
# ~>    from -:5:in `<main>'
``````

Now that we've learned about `nil` returns from the spaceship operator, let's take a look at the spaceship operator in the `Quantity` class we've been developing. Right off the bat, we can see that there is some redundancy here, with the type being checked twice. Oops.

But a bigger problem is that this method raises an exception if the types are incompatible. If we remove that first guard clause, it will return `false` if the operands are incompatible, rather than `nil`.

``````def <=>(other)
raise TypeError unless other.is_a?(self.class)
other.is_a?(self.class) && magnitude <=> other.to_f
end
``````

Let's fix this. We'll make the guard clause return nil instead of failing when the class of the other object is wrong. And we'll remove the redundant type check.

``````def <=>(other)
return nil unless other.is_a?(self.class)
magnitude <=> other.to_f
end
``````

This version of the operator correctly complies with Ruby's conventions for the spaceship operator.

And that's all for today. Special thanks to Jacob Swanner for the comment that prompted this follow-up episode. Happy hacking!