Ruby Spaceship <=> Operator
28 Feb 2016Adhering 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.