Commutative equality in Ruby?

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.

Leave a Reply

Your email address will not be published. Required fields are marked *