Ruby Spaceship <=> Operator

Adhering to the law of trichotomy, the <=> operator (sometimes called the “Spaceship Operator”) works by comparing two elements and returning a -1, 0, or 1. While the original mathematical criteria applies to only real numbers, many programming languages implement the law of trichotomy as a general comparison between equivalent types.

Basics

At first glance, the return value of the <=> operator can be a bit confusing. A simple way to remember the significance of these return values is to break an expression down from left to right:

1. < –> -1

if a < b, then -1 is returned

2. = –> 0

if a = b, then 0 is returned

3. > –> 1

if a > b, then 1 is returned

Example:

> a = 3
> b = 5
> a <=> b
# => -1

> a = 3
> b = 3
> a <=> b
# => 0

> a = 10
> b = 3
> a <=> b
# => 1

Sorting

The <=> operator can be used alone for comparison or its contract honored within a block following the sort method.

By default, the <=> operator behaves as described above:

list = [8, 3, 1, 4, 0, 3]
list.sort { |a, b| a <=> b }
# => [0, 1, 3, 3, 4, 8]

Following this pattern, it is easy to sort a list in reverse by swapping operand positions:

list = [8, 3, 1, 4, 0, 3]
list.sort { |a, b| b <=> a }
# => [8, 4, 3, 3, 1, 0]

The sort method is extendable beyond explicit use of the <=> operator. A block passed to sort must only return either -1, 0, or 1 for sorting to work effectively.

If the same list were to be ordered in odds then evens:

list = [8, 3, 1, 4, 0, 3]
list.sort { |a, _| a.odd? ? -1 : 1 }
# => [3, 3, 1, 4, 8, 0]

Also, since -1, 0, and 1 are simple integers, creating compound <=> blocks is possible. If the same list were to be sorted odds then evens, with all odd and even numbers sorted in ascending order, it might look like this:

list = [8, 3, 1, 4, 0, 3]
list.sort do |a, b|
  if a.odd?
    if b.odd?
      # both are odd, default <=> behaviour is used
      a <=> b
    else
      -1 # a < b
    end
  else # a is even
    if b.even?
      # both are even, default <=> behaviour is used
      a <=> b
    else
      1 # a > b
    end
  end
end
# => [1, 3, 3, 0, 4, 8]

Far from elegant, this code effectively sorts the array of numbers first by odds to evens and then in ascending order.

Custom Methods

While thought of and generally referred to as an operator, <=> is actually a method. Defined originally on Object, any object has the opportunity to redefine this method to achieve custom sort behaviour.

An example class, Node, has a single attribute value:

class Node
  attr_accessor :value
end

By default, sorting an array of Node objects does not produce the desired result:

node1 = Node.new
node1.value = 20

node2 = Node.new
node2.value = 10

node3 = Node.new
node3.value = 30

list = [node1, node2, node3]
list.sort

# => ArgumentError: comparison of Node with Node failed

However, with a custom <=> method:

class Node
  attr_accessor :value

  def <=>(other_node)
    self.value <=> other_node.value
  end
end

list = [node1, node2, node3]
list.sort
# => [#<Node: @value=10>, #<Node: @value=20>, #<Node: @value=30>]

Boom! Custom sorting in the mix!

Admittedly, this same behaviour is possible with the built in sort_by method:

list = [node1, node2, node3]
list.sort_by(&:value)

# => [#<Node: @value=10>, #<Node: @value=20>, #<Node: @value=30>]

But, what happens when the Node class wants to expose a sorting mechanism while removing its value method? In that case, the sort_by method would no longer be sufficient.

Defining a custom <=> method also allows objects to include the Comparable mixin effectively. The Comparable mixin provides the conventional comparison operators (<, >, <=, etc.).

class Node
  include Comparable
  attr_accessor :value

  def <=>(other_node)
    self.value <=> other_node.value
  end
end

node1 = Node.new
node1.value = 200

node2 = Node.new
node2.value = 300

node1 > node2
# => false

node1 < node2
# => true

node3 = Node.new
node3.value = 250

node3.between?(node1, node2)
# => true

The Comparable mixin provides very useful functionality that can easily turn a custom class into a sortable list element. As with most patterns in software, using the right tool at the right time is very important. It may not always be appropriate to write a custom comparison mechanism for a class, but when it is, the collaboration of <=> and the Comparable mixin is worth evaluating.