Feature #1697



Added by marcandre (Marc-Andre Lafortune) over 13 years ago. Updated over 11 years ago.

Target version:


The definition of the <=> operator states:
"... And if the two operands are not comparable, it returns nil" (The Ruby Programming Language, O'Reilly)

Attempting to compare objects, when one or both do not define the <=> operator, causes a NoMethodError to be raised. For example, "false <=> 0" raises in this way. This behavior is unexpected and runs counter to the principle defined above.

Further, "0 <=> false" returns nil. This is fundamentally inconsistent. The two comparisons are the other's converse, yet the raising of an exception in the first case implies that the programmer was in error; that the mere act of making this comparison was erroneous.

The solution is for Object to define a <=> operator. This will solve the case described above, along with the general case of comparing an object to another when one or both do not define <=>. Similarly to Time#<=>, it would return the converse of arg <=> self (i.e. nil => nil, num => -num). It needs to detect recursion, in which case it should return nil or 0 depending on the result of self == arg.

This change would make it always safe to call <=> without having to check first if it respond_to? :<=> (or rescuing NoMethodError).

The existence of Object#<=> would make it much easier for all programmers to define a good <=> operator for their classes. They can simply call super if they don't know how to handle some argument type. For example:

class MyClass
include Comparable
def <=> (arg)
return super unless arg.is_a? MyClass
# go on with comparison

With this simple line, the developper has enabled other classes to be comparable with MyClass. No need to monkeypatch MyClass to ensure that comparing its objects with objects of class ComparableToMyClass will work. Without a 'super', implementing this becomes quite difficult and requires the use of recursion guards (which are not defined in the standard library).

Note that neither String#<=> nor Time#<=> currently use recursion guards, which is not robust and can lead to problems. For instance:

class MyClass
include Comparable
def <=> (arg)
return -1 if arg.is_a? MyClass
cmp = arg <=> self
cmp ? -cmp : nil
end <=>

==> raises a SystemStackError

class Time
alias_method :to_str, :to_s
"now" <=>

==> endless loop that can't be interrupted with ctrl-C.

In summary, defining Object#<=> would:

  1. bring consistency between a <=> b and b <=> a
  2. provide a sensible default (nil) for objects that can't be compared
  3. make it easier for generic methods to call <=> (no rescue or :respond_to? needed)
  4. make it much easier for developpers to write extensible <=> methods for their classes.

Side notes:

The proposition stands only for Object. BasicObject would still be available for developers preferring a class with a strict minimum of methods.

The only code that could break would have to be both checking respond_to? :<=> (or rescuing a NoMethodError) and behaving differently than if the <=> method had returned nil. Such code would be quite nonsensical, given the definition of <=>

Other comparison operators like <, <=, >=, > would also gain in consistency if they were defined in terms of <=>. This way, 0 < nil and nil > 0 would raise the same errors instead of errors of different types. This is secondary to the main question: is it better to define Object#<=> or not?
My vote is on 'yes'.

(Thanks to the readers of my first draft)

Related issues 2 (0 open2 closed)

Related to Ruby master - Bug #2047: Time#<=> Raises NoMethodError on Incomparable ArgumentClosedmatz (Yukihiro Matsumoto)09/05/2009Actions
Related to Ruby master - Bug #2276: Array#<=> should not raise when comparison failsClosed10/26/2009Actions

Also available in: Atom PDF