Introduction
Before we get into larger modules like Enumerable
there are some very interesting ones that don't get quite as much attention. One of them is Comparable
, which allows you to compare and sort items of a similar type.
Difficulty
Foundational
Little to no prerequisite knowledge required. This post focuses on foundational and fundamental knowledge for Ruby programmers.
Comparable
Comparable
is a type of module in Ruby, or in this case a group of methods, that requires you to define one method to get all types of other methods based on it. Some would call this an interface in other languages, and that's a fairly reasonable descriptor of what Comparable
is.
As to what it does? It allows you to compare things, much like the name implies.
What're We Comparing?
To start out with, what are we comparing? Let's work with a card from a Standard 52-Card Hand:
class Card
SUITS = %w(S H D C).freeze
RANKS = %w(2 3 4 5 6 7 8 9 10 J Q K A).freeze
RANKS_SCORES = RANKS.each_with_index.to_h
def initialize(suit, rank)
@suit = suit
@rank = rank
end
def to_s() = "#{@suit}#{@rank}"
end
For me I like to define what type of data goes into our class in constants before actually building out the rest of the class.
In this case we have suits of S H D C, meaning Spades, Hearts, Diamonds, and Clubs. We also have ranks from 2 up through Ace, and since those ranks aren't all numbers we want to know what order they're in and we want a mapping of their relative ranks to eachother. We can check that with Card::RANKS_SCORES
:
Card::RANKS_SCORES
# => {
# "2"=>0, "3"=>1, "4"=>2, "5"=>3, "6"=>4, "7"=>5, "8"=>6,
# "9"=>7, "10"=>8, "J"=>9, "Q"=>10, "K"=>11, "A"=>12
# }
This means A
has a much higher rank, or precedence, than say 2
does.
We also have a quick "To String" method (to_s
) to tell Ruby how to present a Card as a string.
Precedence
Before we compare things, we're going to want to define a method for precedence to encapsulate the idea of an integer-like score for each card:
class Card
def precedence() = RANKS_SCORES[@rank]
end
In this case we can say a card's precedence is based solely on its rank. For some card games though we might also want to include the suit. If that was the case we'd want to add a few things:
class Card
SUITS_RANKS = SUITS.each_with_index.to_h
def precedence
[SUITS_SCORES[@suit], RANKS_SCORES[@rank]]
end
end
Why the Array
? Because it lets us compare on sequential conditions later. First by suit, and then by rank.
The Rocketship Operator
In order to be able to compare things in Ruby we first need to know how to tell if something is larger, smaller, or equivalent to something else of the same type. Ruby encapsulates this idea in the Rocketship Operator (<=>
):
def <=>(other)
# ...
end
If we were to implement it for our card it would look a bit like this:
class Card
def <=>(other) = precedence <=> other.precedence
end
Because we defined a precedence
method earlier our implementation is pretty easy. We're comparing the precedence of the two cards.
How is this useful? Well let's take a look at the return value:
two_of_spades = Card.new('S', '2')
three_of_spades = Card.new('S', '3')
four_of_spades = Card.new('S', '4')
two_of_spades <=> three_of_spades
# => -1 - Greater right side
four_of_spades <=> four_of_spades
# => 0 - Equal values
three_of_spades <=> two_of_spades
# => 1 - Greater left side
It gives us back three integers ranging from -1 to 1, which really doesn't seem that handy, until you remember that some operators like ===
are more useful for what they enable rather than being used explicitly themselves.
Introducing Comparable
So where does Comparable
fit in to all of this? Right here:
class Card
include Comparable
end
Adding that one line along with a Rocketship Operator (<=>
) lets us do a whole bunch of things like:
def show_hand(cards) = cards.map(&:to_s).join(', ')
cards = ('2'..'8').map { Card.new('S', _1) }.shuffle
show_hand cards
# => "S6, S2, S5, S7, S3, S4, S8"
show_hand cards.sort
# => "S2, S3, S4, S5, S6, S7, S8"
show_hand cards.max(2)
# => "S8, S7"
show_hand cards.minmax
# => "S2, S8"
cards.min.to_s
# => "S2"
...and quite a few more you can find in the Comparable docs. One module included and one method defined for all of that seems like a pretty good deal to me!
Wrapping Up
Comparable
is one of several useful interface modules in Ruby, and knowing how it and the Rocketship Operator (<=>
) work can be very useful indeed.
Next round we'll be taking a look at one of the largest interface modules, and likely the biggest center of power for most of what makes Ruby Ruby: Enumerable
.
Want to keep up to date on what I'm writing and working on? Take a look at my new newsletter: The Lapidary Lemur
Top comments (2)
The way that Comparable and Enumerable unlock all this functionality is such a great feature of Ruby. In JavaScript, methods like map, select, find, etc are all tied to Array, which means you can’t treat Sets, Arrays and Lists all the same. It’s so disappointing.
If taken to the next conclusion it can be somewhat annoying that if you
map
aHash
or other data types you get back anArray
. Still doing some musing on a nice interface to fix that.