Bug #18937
closedInconsistent definitions of Complex#<=> and Numeric#<=> with others
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) over 2 years 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) over 2 years 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) over 2 years ago
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 fromObject#<=>
, 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
Updated by nobu (Nobuyoshi Nakada) over 2 years ago
I agree that Complex#<=>
should coerce the argument as well as other methods/classes.
https://github.com/ruby/ruby/pull/6269
Updated by nobu (Nobuyoshi Nakada) over 2 years ago
- Status changed from Open to Closed
Applied in changeset git|d5f50463c2b5c5263aa45c58f3f4ec73de8868d5.
[Bug #18937] Coerce non-Numeric into Complex at comparisons
Updated by msnm (Masahiro Nomoto) about 2 years ago
nobu (Nobuyoshi Nakada) wrote in #note-4:
I agree that
Complex#<=>
should coerce the argument as well as other methods/classes.
https://github.com/ruby/ruby/pull/6269
Thanks @nobu (Nobuyoshi Nakada) .
But #coerce
is not called when a custom numeric class has def real? = false
following Complex#real?
.
puts RUBY_DESCRIPTION
# ruby 3.2.0dev (2022-08-22T03:26:43Z :detached: d5f50463c2) [x86_64-linux]
class MyInteger < Numeric
def initialize(n) = (@n = n)
def coerce(other)
puts "MyInteger#coerce is called."
[other, @n]
end
end
class MyComplex < Numeric
def initialize(n) = (@n = Complex(n))
def real? = false
def coerce(other)
puts "MyComplex#coerce is called."
[other, @n]
end
end
p Complex(1) <=> MyInteger.new(2)
# MyInteger#coerce is called.
#=> -1
p Complex(1) <=> MyComplex.new(2)
#=> nil
# FYI: Complex#+ works fine.
p Complex(1) + MyComplex.new(2)
# MyComplex#coerce is called.
#=> (3+0i)