Today I discovered Ruby doing something unexpected with the ==
operator. With a class like this:
class Value # ... initialize, other methods, etc. ... def ==(other) self.a_method == other.a_method end end
and a test that effectively evaluated the expression:
3 == Value.new(args)
I expected a message saying that Value
couldn’t be coerced to Fixnum
. Instead I got
undefined method `a_method' for 3:Fixnum (NoMethodError)
I thought that 3 == Value.new(args)
was equivalent to 3.==(Value.new(args))
, yet this message suggests Ruby is evaluating Value.new(args).==(3)
. It’s almost as if Ruby has decided that ==
is commutative. How can that happen? I was obviously missing something.
Googling around confirmed what I thought about how operator expressions are evaluated, and showed that most reported problems were from people finding out that Ruby operators aren’t commutative by default, not the other way around. I wondered if there was some type conversion or similar going on. I consulted my old version of the Pickaxe book which talked about 3 conversion protocols: loose methods like to_i
and to_s
; strict methods like to_int
and to_str
; and double-dispatch via the coerce
method. None of these could be at play in my Value
type.
Experimenting with a simpler class NoisyOperators
class NoisyOperators def +(other) puts "Adding 3 to #{other.inspect}" 3 + other end def ==(other) puts "Comparing NoisyOperators #{self.inspect} to #{other.inspect}" 3 == other end end
I found that putting a NoisyOperator
instance first in an addition works fine
irb> NoisyOperators.new() + 4 Adding 3 to 4 => 7
while its commutation fails as expected
irb> 4 + NoisyOperators.new() TypeError: NoisyOperators can't be coerced into Fixnum from (irb):3:in `+' from (irb):3 from /home/mick/.rvm/rubies/ruby-2.0.0-p247/bin/irb:13:in `<main>'
However, the ==
operator seems special, exhibiting the strange commutation I saw earlier.
irb> NoisyOperators.new() == 3 Comparing NoisyOperators #<NoisyOperators:0x000000027756d0> to 3 => true irb> 3 == NoisyOperators.new() Comparing NoisyOperators #<NoisyOperators:0x0000000277d858> to 3 => true
The “Comparing NoisyOperators …” message indicates that same method is called in both cases, and with the same argument. One other thing I noticed was that it might only be numbers that are affected:
irb> NoisyOperators.new() == Object.new Comparing NoisyOperators #<NoisyOperators:0x000000027a29f0> to #<Object:0x000000027a29c8> => false irb> Object.new == NoisyOperators.new() => false
When an Object
instance comes first, the “Comparing NoisyOperators …” message is not printed, which suggests the NoisyOperators#==
method is not called.
At this point I started writing a StackOverflow question. Before I could finish it the related questions list seemed to do much better than my initial searches and turned up this answer. It says that Fixnum#==
method will delegate to its argument’s ==
method if that argument is not an instance of Numeric
. A quick glance at the source code for Ruby seems to confirm it.
So, it seems the Numeric#==
methods are in fact commutative thanks to them doing their own double-dispatch to find a suitable implementation. It seems odd that they do this outside of the protocol that uses the #coerce
method, and even odder that it isn’t mentioned in any documentation I could find.