Scala has co-operative equality. This means that equality between numeric values is kept the same if the values are abstracted to Any
:
scala> 1 == 1L
res0: Boolean = true
scala> (1: Any) == (1L: Any)
res1: Boolean = true
scala> (1: Any).equals(1L: Any)
res2: Boolean = false
The transcript shows that equality ==
on Any
is not the same as equals
. Indeed the ==
operator is treated specially by the compiler and leads to quite complicated code sequences. The same holds for the hash operator ##
which is also more complex than hashCode
. This has a price - it’s the primary reason why most sets and maps in Scala are significantly slower than equivalent data structures in Java (factors of up to 5 were reported, but I won’t vouch for their accuracy).
Now, why was cooperative equality added to Scala? This was not my idea, so I can only try to reconstruct the motivation. I believe the main reason was that it was felt that 1 == 1L
should be the same as (1: Any) == (1L: Any)
. In other words, boxing should be transparent.
The problem with reasoning is that this tries to “paper over” the true status of ==
in Scala. In fact ==
is an overloaded method. There is one version on Any
, and others on Int
, Long
, Float
, and so on. If we look at it in detail the method called for 1 == 1L
is this one, in class Int
:
def ==(x: Long): Boolean
If we write (1: Any) == (1L: Any)
, it’s another ==
method, which is called. This used to be just the method postulated on Any
:
final def == (that: Any): Boolean =
if (null eq this) null eq that else this equals that
But with co-operative equality, we assume there’s an override of this method for numeric value types. In fact the SLS is wrong in the way this is specified. It says that equals
is overriden for numeric types as follows:
That is, the equals method of a numeric value type can be thought of being defined as follows:
def equals(other: Any): Boolean = other match {
case that: Byte => this == that
case that: Short => this == that
case that: Char => this == that
case that: Int => this == that
case that: Long => this == that
case that: Float => this == that
case that: Double => this == that
case _ => false
}
This is demonstratively false:
scala> 1.equals(1L)
res3: Boolean = false
So, the conclusion seems to be that the compiler somehow treats ==
on Any as a combination of the numeric equals
with the fallback case of general equals for non-numeric types.
The question is: Do we want to keep it that way? The current treatment seems to be both irregular and expensive. Are there other benefits that I have overlooked? And, how difficult would it be to move away from cooperative equality?