3

I'm going through some tutorials on CodeAcademy, and came across this scenario:

books = ["Charlie and the Chocolate Factory", "War and Peace", "Utopia", "A Brief History of Time", "A Wrinkle in Time"]

# To sort our books in ascending order, in-place
books.sort! { |firstBook, secondBook| firstBook <=> secondBook }


# Sort your books in descending order, in-place below
# this lin initially left blank
books.sort! {|firstBook, secondBook| secondBook <=> firstBook}

Instead of using if/else blocks, I gave this a shot, and it worked, but I don't know why. I assume it doesn't matter which order you place the items in the check (i.e., a <=> b vs. b <=> a). Could someone explain what's happening here?

sawa
  • 165,429
  • 45
  • 277
  • 381
MrDuk
  • 16,578
  • 18
  • 74
  • 133
  • if you reverse the items then the sorting changes the order like Descending and Ascending – uday Nov 27 '13 at 16:01
  • 1
    I *HOPE* a tutorial for Ruby didn't suggest you use camelCase for your variables. In Ruby we use snake_case for variables. – the Tin Man Nov 27 '13 at 16:46
  • I can't really remember - it's just personal preference either way. I really dislike seeing _ in variable names. – MrDuk Dec 02 '13 at 13:55

3 Answers3

5

If you reverse the elements in <=> you reverse its value. If the elements are equal this operator returns 0, but if the first one is smaller it returns a negative value, if the first is greater it returns a positive value. Thus if temp = a <=> b then b <=> a is -temp. So you reverse the order of sorting if you write the arguments in reverse order.

Ivaylo Strandjev
  • 69,226
  • 18
  • 123
  • 176
  • Thanks, but I'm still slightly confused - maybe my question should have been, what's the significance of -1 vs 1? I get that -1 is less than the object and 1 is greater, but how does the sorting strategy handle that value once we've passed it? – MrDuk Nov 27 '13 at 16:02
  • 1
    @cote The so-called spaceship-operator `<=>` is the fundamental comparator. It's used to establish what sort order two elements should have relative to the other. If `-1` then the first comes before the second, if `1` then the first comes after the second. If `0` they are considered to be exactly the same from a sorting perspective and no ordering is applied. – tadman Nov 27 '13 at 16:06
  • 1
    @ctote imagine you have a sorting algorithm that does something if `a` is less than `b`. Than if you reverse the order of the arguments of this operator the algorithm will perform the same operations but its notion of `a` is less than `b` will be reversed. So whenever `a` was less than `b` in the first case it will turn out to be greater in the second. Try and write down a simple bubble sort. What will happen if your comparison `a < b` is reversed i.e. returns true iff `a > b` – Ivaylo Strandjev Nov 27 '13 at 16:06
  • @ctote If you are confused, then try not to use it, and use `sort_by!` instead, which uses Schwartzian transform. That is easier to understand, and is more efficient. Descending order can be acheived by `reverse`. There is not much reason to use `sort!`. – sawa Nov 27 '13 at 16:09
  • `sort_by` is more efficient when sorting complex objects. Simple objects, like strings and integers, will be faster using `sort`. That's because `sort_by` has overhead in order to perform the Schwartzian transform. It's masked by the method name, but look at the underlying code and you'll see the set-up and tear-down of the elements being sorted has a cost associated with it. – the Tin Man Nov 27 '13 at 17:01
3

Here's some simple visual ways of seeing what <=> does, and how reversing the order of the comparison variables affects the order of the output.

Starting with a basic Array:

foo = %w[a z b x]

We can do an ascending sort:

foo.sort { |i, j| i <=> j } # => ["a", "b", "x", "z"]

Or a descending sort by reversing the two variables being compared:

foo.sort { |i, j| j <=> i } # => ["z", "x", "b", "a"]

The <=> operator returns -1, 0 or 1, depending on whether the comparison is <, == or > respectively.

We can test that by negating the result of the comparison, which will reverse the order if the theory holds true.

foo.sort { |i, j| -(i <=> j) } # => ["z", "x", "b", "a"]
foo.sort { |i, j| -(j <=> i) } # => ["a", "b", "x", "z"]

By negating the result of the comparisons the order does reverse. But, for clarity in code, just reverse the order of the variables.

That all said, using sort, or its destructive sibling sort!, isn't always the fastest way to sort complex objects. Simple objects, like strings and characters, and numerics, sort extremely quickly because their classes implement the necessary methods to perform <=> tests quickly.

Some of the answers and comments mention sort_by, so let's go there.

Complex objects don't usually sort correctly, so we end up using getters/accessors to retrieve some value we want to compare against, and that action has a cost in CPU time. sort repeatedly compares the values so that retrieval would occur repeatedly, and add up as wasted time when sorting wasn't happening.

To fix that, a smart guy named Randall Schwartz, who's a major player in the Perl world, started using an algorithm that precomputes once the value to be used to sort; That algorithm is commonly called a Schwartzian Transform as a result. That value, and the actual object, are bundled together in a small sub-array, and then sorted. Because the sort occurs against the pre-computed value, it, and its associated object, are moved around in the ordering, until the sort completes. At that point, the actual objects are retrieved and returned as the result of the method. Ruby implements that type of sort using sort_by.

sort_by doesn't use <=> externally, so you can sort by simply telling it how to get at the value you want to compare against:

class Foo
  attr_reader :i, :c
  def initialize(i, c)
    @i = i
    @c = c
  end
end

Here's the array of objects. Note that they are in the order they were created, but not sorted:

foo = [[1,  'z'], [26, 'a'], [2,  'x'], [25, 'b'] ].map { |i, c| Foo.new(i, c) }
# => [#<Foo:0x007f97d1061d80 @c="z", @i=1>,
#     #<Foo:0x007f97d1061d58 @c="a", @i=26>,
#     #<Foo:0x007f97d1061d30 @c="x", @i=2>,
#     #<Foo:0x007f97d1061ce0 @c="b", @i=25>]

Sorting them by the integer value:

foo.sort_by{ |f| f.i } 
# => [#<Foo:0x007f97d1061d80 @c="z", @i=1>,
#     #<Foo:0x007f97d1061d30 @c="x", @i=2>,
#     #<Foo:0x007f97d1061ce0 @c="b", @i=25>,
#     #<Foo:0x007f97d1061d58 @c="a", @i=26>]

Sorting them by the character value:

foo.sort_by{ |f| f.c } 
# => [#<Foo:0x007f97d1061d58 @c="a", @i=26>,
#     #<Foo:0x007f97d1061ce0 @c="b", @i=25>,
#     #<Foo:0x007f97d1061d30 @c="x", @i=2>,
#     #<Foo:0x007f97d1061d80 @c="z", @i=1>]

sort_by doesn't respond as well to using a negated value as sort and <=>, so, based on some benchmarks done a while back on Stack Overflow, we know that using reverse on the resulting value is the fastest way to switch the order from ascending to descending:

foo.sort_by{ |f| f.i }.reverse
# => [#<Foo:0x007f97d1061d58 @c="a", @i=26>,
#     #<Foo:0x007f97d1061ce0 @c="b", @i=25>,
#     #<Foo:0x007f97d1061d30 @c="x", @i=2>,
#     #<Foo:0x007f97d1061d80 @c="z", @i=1>]

foo.sort_by{ |f| f.c }.reverse 
# => [#<Foo:0x007f97d1061d80 @c="z", @i=1>,
#     #<Foo:0x007f97d1061d30 @c="x", @i=2>,
#     #<Foo:0x007f97d1061ce0 @c="b", @i=25>,
#     #<Foo:0x007f97d1061d58 @c="a", @i=26>]

They're somewhat interchangable, but you have to remember that sort_by does have overhead, which is apparent when you compare its times against sort times when running against simple objects. Use the right method at the right time and you can see dramatic speed-ups.

Community
  • 1
  • 1
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
0

Its called a spaceship operator

If you have something like this

my_array = ["b","c","a"]

my_array.sort! does the compare the elements of the array since it knows that the letters of english alphabet have natural ordering, likewise if you have array of integers

my_array2 = [3,1,2]

my_array2.sort! will compare the elements and gives the result as [1,2,3]

but if you want to change how the comparison is made in an array of strings or complex objects you specify it using the <=> operator..

my_array3 = ["hello", "world how are" , "you"]

my_array3.sort! { |first_element, second_element| first_element <=> second_element }

so it will tell the sort method to compare like this:

Is first_element < second_element?

Is first_element = second_element?

Is first_element > second_element?

but if you take this stmt,

my_array3.sort! { |first_element, second_element| first_element <=> second_element }

the comparison is made as follows:

Is second_element < first_element?

Is second_element = first_element?

Is second_element > first_element?

So it does make a difference if you change the elements to be considered.

uday
  • 8,544
  • 4
  • 30
  • 54
  • It's *commonly* called the "space-ship operator". It's always a binary-comparison operator in Perl, which is where Ruby inherited it from. – the Tin Man Nov 27 '13 at 16:51
  • Also, use snake_case, not camelCase for variables in Ruby. And, your examples don't work. Use `first_element` and `second_element` and make sure your assignments and variable spellings are consistent. It's a good idea to use IRB to test your examples, and then copy and paste them once they work. – the Tin Man Nov 27 '13 at 16:58
  • "...an array of strings or complex objects then it doesnt know how to compare those objects"? Ruby knows how to compare strings just like it does single characters, which are strings. `%w[foo bar].sort # => ["bar", "foo"]`. – the Tin Man Nov 27 '13 at 17:38