Why doesn't sort or the spaceship (flying saucer) operator (<=>) work on booleans in Ruby?
Asked Answered
U

4

16

In "Is it possible to sort a list of objects depending on if the individual object's response to a method?", I discovered that the flying saucer doesn't work on booleans.

Consider:

Ruby 1.8.7:

[true, false].sort # => undefined method `<=>' for true:TrueClass (NoMethodError)
true <=> false     # => undefined method `<=>' for true:TrueClass (NoMethodError)

Ruby 1.9.3:

[true, false].sort # => comparison of TrueClass with false failed (ArgumentError)
true <=> false     # => nil
true <=> true      # => 0
false <=> true     # => nil

It may have something to do with true and false not having a canonical sort order, because which comes first? But, that sounds pretty weak to me.

Is this a bug in sort?

Urban answered 11/2, 2013 at 16:13 Comment(13)
Don't attribute your misunderstanding to Ruby's fault (bug).Ragtime
Only Matz can answer this question correctly...Yakka
@maerics, Fair enough. @sawa, the reason I thought it's a bug is that true and false now implement <=> but violate the expectation (implicit contract) that <=> returns a value suitable for sort.Urban
Then perhaps the question should be, why bother to implement '<=>' for booleans at all?Breastfeed
Yeah, exactly! It seems that in Ruby 1.9, <=> was added to Object ("Returns 0 if obj === other, otherwise nil.") and it's that implementation that's confusing sort.Urban
@Urban I see. That is kind of weird. <=> should always return 0, -1, or 1, or be undefined. Returning nil is violationg the expectation. I agree.Ragtime
@sawa: It may well be that nil is the designers' implementation for 'undefined' (similar to NULL). The alternative would be to throw an exception - perhaps there are scenarios where returning a legitimate value would be preferred to forcing the developer to catch the exception...Breastfeed
@Ragtime From the Comparable doc: "If the other object is not comparable then the <=> operator should return nil."Zebrass
@sawa: The return values for <=> are -1, 0, 1 and nil for less-than, equal, greater-than and not-comparable. That's the standard protocol for <=>, and I don't see how the implementation for booleans violates that protocol. true is equal to true, but it is not comparable to false.Manganese
I don't feel true <=> true being 0 strange. I felt uncomparable things returning nil to violate expectations that we had during 1.8, but now, it changed in 1.9, and that may be another way of thinking.Ragtime
Comparable! Aha! So what sort is saying is "hey doofus, true and false are not comparable; try sorting some objects that implement the Comparable module instead" but it comes out "comparison of TrueClass with false failed" which is not nearly as helpful.Urban
It's called the spaceship operator.Spreader
@AndrewGrimm okay fine, I changed the question title :-) (I prefer flying saucer though!)Urban
Y
12

Boolean values have no natural ordering.

The Ruby language designer(s) probably felt that to invent an ordering for booleans would be a surprise to developers so they intentionally left out the comparison operators.

Yakka answered 11/2, 2013 at 16:29 Comment(6)
Yup. Except that <=> is implemented even though < and > are not. It seems that in Ruby 1.9, <=> was added to Object ("Returns 0 if obj === other, otherwise nil.") and it's that implementation that's confusing sort for true and false.Urban
@AlexChaffee: ah yes, I see, so this means that booleans have a busted spaceship operator. Lame!Yakka
@maerics: The implementation of <=> for booleans is in full compliance with the protocol for <=>. In what way is it "busted"?Manganese
@JörgWMittag: hmm, I assumed that the <=> operator was for comparisons ala sort (returning values -1, 0, 1) but upon reading more documentation (e.g. Numeric) it seems that this is not the case. By "busted" I mean confusing to me because it violated what I thought was the convention of the spaceship operator implementing comparisons.Yakka
btw I'm accepting this answer but should note that the complete story is actually inside the comments on the question itselfUrban
That's just the wrong decision. A Boolean algebra is basically a lattice, and top and bottom (true and false) are definitely comparable.Smoky
B
9

The so-called flying saucer requires all comparison operators (<, >, ==) to work (not technically, although certainly theoretically). true and false are not less-than or greater-than each other. The same will hold true for nil. For a practical workaround, you can 'cast' to integers (0 for false, 1 for true). Something like:

[true, false, true].sort_by{|e| e ? 1 : 0}
Breastfeed answered 11/2, 2013 at 16:17 Comment(6)
I understand that "true and false are not less-than or greater-than each other" -- I'm asking why was Ruby designed that way? It seems much more useful to be able to sort an array of booleans using a simple [true, false].sort or @products.sort_by(&:on_sale?) than to have a sudden failure case in a language that's usually so resilient.Urban
As a design question, it's likely because one can't really guess the developer's intent. What value would you expect to see when comparing true > false? In some languages false is equated with 0, while true might be 1 or -1. It's an arbitrary decision that the designers probably felt should be made by the developer. As an aside, PHP tried to do something about this and probably made it more confusing in the process...Breastfeed
The flying saucer does not require all comparison operators to work. On the contrary, with <=> defined and the Comparable module included, all comparison methods are defined 'for free'.Zebrass
@steenslag: technically correct but the idea remains the same. The implementation for <=> should be representative for the functionality of the comparison operators. Updated to clarify this - thanks for pointing this out.Breastfeed
@AlexChaffee: Why would you expect Ruby to be able to sort something which cannot be sorted?Manganese
btw even though you have a good solution, I'm not accepting this answer because it contains a falsehood. <=> doesn't require <, >, etc., and Comparable implements <, >, etc. in terms of <=>.Urban
U
2

Booleans have no natural ordering. Unlike C, false is not less than true, they're just equivalent and equally valid states. However it is possible to configure the sort any way you like using a block, for example:

ary = [true, false, false, true]
ary.sort {|a,b|  a == b ? 0 : a ? 1 : -1 }

# => [false, false, true, true]

Reversing the order is also trivial:

ary.sort {|a,b|  a == b ? 0 : a ? -1 : 1 }

# => [true, true, false, false]
Untwine answered 3/12, 2013 at 12:8 Comment(0)
P
0

I know this is quite old, but this bit me recently. But it's Ruby, right? How about doing this monkey-patch?

#! /usr/bin/env ruby

class TrueClass
  include Comparable

  def <=>(other)
    if other.class == FalseClass
      1
    elsif other.class == TrueClass
      0
    else
      nil
    end
  end
end

class FalseClass
  include Comparable

  def <=>(other)
    if other.class == TrueClass
      -1
    elsif other.class == FalseClass
      0
    else
      nil
    end
  end
end

puts "true <=> false: #{true <=> false}"
puts "false <=> true: #{false <=> true}"
puts "true <=> true: #{true <=> true}"
puts "false <=> false: #{false <=> false}"
puts "true <=> 13: #{true <=> 13}"
puts "true > false: #{true > false}"
puts "true < false: #{true < false}"
puts "true == false: #{true == false}"
puts "true == true: #{true == true}"
puts "false == false: #{false == false}"
puts "false < true: #{false < true}"
puts "[false, true, false].max: #{[false, true, false].max}"
puts "[false, true, false].min: #{[false, true, false].min}"
Plectron answered 21/1, 2022 at 10:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.