Ruby: .max, .max_by, & the Spaceship!

Conrad Buys
6 min readSep 10, 2020

--

Today, we’re gonna go to space. While also learning how to use a suite of array enumerations! Let’s learn to love the <=>.

An essential part of programming is knowing your comparison operators. These are things like == (checking equality), > (greater than), < (less than), etc. They’re staples of mathematics & boolean logic, and comprise a whole lot of our code.

However, you might not know all of the comparison operators out there. I wouldn’t blame you — there are a LOT of these bad boys out there across many languages. One that you may have seen & scratched your head at is the “three way comparison”.

<=>

Whoa. What the heck is that thing?

The spaceship is not the work of an enemy stand.

We affectionately refer to this bad boy as the “spaceship” operator, & its usage is way less intimidating than it may look at first glance! Since it is a comparison operator, it is used to compare two pieces of data. However, instead of a simple true/false return, it has three states of return, based on the three characters it contains.

Let’s take an example: a <=> b.

  • If a is less than b (a < b), the comparison will return -1.
  • If a is equal to b (a == b), the comparison will return 0.
  • If a is greater than b (a > b), the comparison will return 1.

Ok Conrad. That’s neato. But how do we use that? I can’t use that in my if statements, since those require true/false. Well, you’d be partially right! Technically, you can use them in your if statements, it would just require an extra step (ex. if (a <=> b) == 0 is the same as if a == b, but with extra steps.) But the real meat & potatoes of the spaceship is with some enumerators that Ruby provides for us! We’re gonna cover 3 things here: .max, . max_by vs .max, & other enumerables that use the spaceship!

.max

Let’s say you need to find the highest integer in an array of data. There are a number of ways we can go about this, but let’s use .max! A statement using .max may look something like this:

array = [0, 4, 18, 6]array.max do |a, b|
a <=> b
end
=> 18

Okay, cool. We’ve successfully used our first spaceship! Give yourself a pat on the back! But… huh? What’s a & b in this example? And how is this actually working? I’m glad you asked, friend! An effective way to visualize .max is as a fighting ring. Put up your dukes.

Let’s think of our array of numbers as a series of challengers (I have money on 6 being our champion. He’s such a good underdog story.) & rewrite our code to fit that analogy.

array.max do |challenger, champion|
challenger <=> champion
end

Challenger vs. champion, only one person leaves the ring. By default, the 0th element of our array will start as champion. Then, max will iterate through the array, pitting each challenger (the next index) against the reigning champion. At the end of each fight, whoever wins (is the bigger number) will become our new champion, and move onto the next round! This will continue until we’ve iterated through the entire array, and eventually found our grand champion (ie, the highest value in the array.)

If we were to visualize it:

  • Champion (array[0]) fights challenger (array[1])
  • Champion of last match fights challenger (array[2])
  • Champion of last match fights challenger (array[3])
  • etc. until we've reached the end of the array.

That makes a whole lot of sense, yeah? Now that we’ve learned the nitty-gritty of how our spaceship works inside of max, let’s simplify things

  • A tie between fighters will always go to the champion. (if we’re getting technical, it’ll go to whoever is on the right side of the spaceship.)
  • You don’t need to write the whole block if you’re comparing an array of integers. For example, [0..2].max (that’s the same as [0,1,2].max) will return 2, without any of the spaceship-y fanfare that we wrote before.
  • If you use .max to compare an array of strings, make sure to specify that you’re comparing length. Otherwise, you’ll be comparing the first character of each string, which goes into some weird Unicode nonsense we don’t have time for today.
  • If you try to compare an array of different data types, you’ll throw an error. Don’t try to compare different types, silly.
  • If you add an argument to .max, it’ll return the top x values. For example, [0, 14, 8, 7].max(2) will return [14, 8].

.max_by vs .max

The .max_by enumerable is a “simplified” version of .max. We’re still going to be iterating through an array and returning the highest element, but no spaceship is gonna be used in our block. Very sad.

Mfw we’re not using the spaceship in our enumerator.

Let’s say that we want to find the string in an array of strings with the highest character count. We’d write it like this.

name_array = [“bob”, “conrad”, “rick”]name_array.max_by do |name|
name.length
end

Basically, max_by will find the highest value based on the criteria inside the block (in this case, the length of each name). Writing this as a .max looks very similar:

name_array.max do |name_challenger, name_champion|
name_challenger.length <=> name_champion.length
end

The first thing I thought when I learned about .max_by was, “okay, so why would I ever use .max instead? .max_by is a lot less writing for the same result.” And this was a fantastic question. From my understanding, you should use .max when the variables you’re comparing are different based on whether you’re the champion or challenger. For example, sticking with our fighting ring example:

people_array.max do |challenger, champion|
challenger.max_hp <=> champion.current_hp
end

In this example, we’re pitting our challenger’s maximum HP against our champion’s current HP — two separate values. This is pretty appropriate to our metaphor: our champion isn’t gonna be in top shape if he doesn’t get a break and keeps fighting! (let’s ignore the fact that his HP isn’t actually going down after each loop in our code… keep our metaphor simple!)

If I just wanted to compare maximum HP, then yes, a .max_by loop would be the right call.

Similar enumerables & examples

Here’s a list of enumerables that work almost identically to .max & .max_by.

  • .min/.min_by: Returns the minimum value.
  • .minmax/.minmax_by: Returns both the minimum & maximum values as an array. Min is element 0 of the returned array, max is element 1. You cannot specify how many mins/maxes to output like you could with .max/.min.
  • .sort/.sort_by: Returns the array in order from min to max. Keep in mind that, according to the Ruby Documentation, “When the comparison of two elements [is equal], the order of the elements is unpredictable.”

Let’s create some examples, just to solidify everything in your head!

numbers = [9, 832, 5, 11, 11, 2]
names = ["bob", "conrad", "melinda", "me"]
numbers.max(2) => [832, 11]
numbers.min => 2
numbers.minmax => [2, 832]
numbers.sort => [2, 5, 9, 11, 11, 832]
names.max_by {|name| name.length} => "melinda"
names.min(3) {|challenger, champion| challenger.length <=> champion.length} => ["me", "bob", "conrad"]

Congrats! We’ve learned about the spaceship & its enumerators! I’ll see you guys on Jupiter!

--

--