Project

General

Profile

Actions

Bug #18937

open

Inconsistent definitions of Complex#<=> and Numeric#<=> with others

Added by msnm (Masahiro Nomoto) 15 days ago. Updated 12 days ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:109311]

Description

Object#<=> says "Returns 0 if obj and other are the same object or obj == other" .
https://ruby-doc.org/core-3.1.2/Object.html#method-i-3C-3D-3E

However, neither Complex#<=> nor Numeric#<=> satisfies that definition.

num1 = Complex(0, 42)
num2 = Complex(0, 42)
p num1.equal?(num2)  #=> false
p num1 == num2       #=> true

# using Complex#<=>
p num1 <=> num2  #=> nil

# using Numeric#<=>
Complex.remove_method(:<=>)
p num1 <=> num2  #=> nil

# using Object#<=> (Kernel#<=>)
Numeric.remove_method(:<=>)
p num1 <=> num2  #=> 0

Complex#<=> has another problem that it does not coerce numeric objects while Integer#<=> and Float#<=> do.
This prevents users from adding yet another complex class having #<=>.


Here is my proposal of Complex#<=> behavior (in Ruby).
This considers #15857, complex numbers are comparable when their imaginary parts are 0.

class Complex
  def <=>(other)
    return (self == other ? 0 : nil) if self.imag != 0

    if other.kind_of?(Complex)
      if other.imag == 0
        return self.real <=> other.real
      else
        return nil
      end
    elsif other.kind_of?(Numeric) && other.real?
      return self.real <=> other
    elsif other.respond_to?(:coerce)
      num1, num2 = other.coerce(self)
      return num1 <=> num2
    else
      return nil
    end
  end
end

Updated by msnm (Masahiro Nomoto) 15 days ago

I fix the last code as follows:

class Complex
  def <=>(other)
    return (self == other ? 0 : nil) if self.imag != 0

    if other.kind_of?(Complex)
      if other.imag == 0
        return self.real <=> other.real
      else
        return nil
      end
    end

    return self.real <=> other
  end
end

Updated by mame (Yusuke Endoh) 12 days ago

However, neither Complex#<=> nor Numeric#<=> satisfies that definition.

Complex#<=> is a different method from Object#<=>, so I don't see that as a problem in itself. Some Ruby core methods don't necessarily follow the Liskov Substitution Princple. I think this is one example of such cases that we favors mathematical intuition over the principle. If you are facing any practical problem, please elaborate the situation.

Complex#<=> has another problem that it does not coerce numeric objects while Integer#<=> and Float#<=> do.
This prevents users from adding yet another complex class having #<=>.

I understand this as follows.

class MyInteger
  def initialize(n)
    @n = n
  end

  def coerce(obj)
    [obj, @n]
  end
end

p 1    <=> MyInteger.new(2) #=> -1
p 1+0i <=> MyInteger.new(2) #=> expected: -1, actual: nil

Updated by msnm (Masahiro Nomoto) 12 days ago

@mame (Yusuke Endoh)

I have a motivation to create a "quaternion" class which is highly interoperable with other built-in numeric classes. (gem: quaternion_c2)

I recently noticed that there was Complex#<=> in Ruby >= 2.7 . I want to implement Quaternion#<=> though I believe few people compare complex numbers by #<=>. However, Complex#<=> feels odd and I can't define Quaternion#<=> well.


Complex#<=> is a different method from Object#<=>, so I don't see that as a problem in itself. Some Ruby core methods don't necessarily follow the Liskov Substitution Princple. I think this is one example of such cases that we favors mathematical intuition over the principle. If you are facing any practical problem, please elaborate the situation.

Thanks. I changed my understanding: "(non-real) complex numbers are not comparable, then Complex#<=> for them always returns nil even if self.equal?(other)".

Complex::I <=> Complex::I  #=> nil

How about Numeric#<=> ? I think num1 == num2 (equivalency) is more intuitive than num1.equal?(num2) (identity) in mathematical contexts. Another idea is that the method always returns nil in order to tell "the custom numeric class may not be comparable".


This prevents users from adding yet another complex class having #<=>.

I understand this as follows.

That's right. Moreover, MyInteger#<=> will produce asymmetric (not antisymmetric) behaviors.

class MyInteger
  def initialize(n)
    @n = n
  end

  def coerce(obj)
    [obj, @n]
  end

  def <=>(obj)
    if obj.kind_of?(MyInteger)
      @n <=> obj.instance_variable_get(:@n)
    else
      @n <=> obj
    end
  end
end

my_int = MyInteger.new(2)
p 1 + 0i <=> my_int  #=> nil (expected: -1)
p my_int <=> 1 + 0i  #=> 1
Actions

Also available in: Atom PDF