After reading examples from the "Programming in Scala" book, chapter 30 on object equality, I've got a little confused on how the reflexivity is guaranteed for equals method in generic containers, considering that NaN comparison is non-reflexive. Consider the following snippet:
class Wrapper[T](val elem: T) {
override def equals(other: Any): Boolean = other match {
case that: Wrapper[_] => this.elem == that.elem
case _ => false
}
}
object NaNCompare extends App {
val nan: Double = 0.0 / 0.0
val nanw: Wrapper[Double] = new Wrapper(nan)
val pzero: Double = +0.0
val pzerow: Wrapper[Double] = new Wrapper(pzero)
val mzero: Double = -0.0
val mzerow: Wrapper[Double] = new Wrapper(mzero)
println(s"nan equals nan: ${nan equals nan}")
println(s"nan == nan: ${nan == nan}")
println(s"+0 equals -0: ${pzero equals mzero}")
println(s"+0 == -0: ${pzero == mzero}")
println(s"[nan] equals [nan]: ${nanw equals nanw}")
println(s"[nan] == [nan]: ${nanw == nanw}")
println(s"[+0] equals [-0]: ${pzerow equals mzerow}")
println(s"[+0] == [-0]: ${pzerow == mzerow}")
}
This prints the following:
nan equals nan: true
nan == nan: false
+0 equals -0: false
+0 == -0: true
[nan] equals [nan]: true
[nan] == [nan]: true
[+0] equals [-0]: true
[+0] == [-0]: true
The first four lines are logical: according to IEEE 754, nan == nan is false, while +0 == -0 is true; however, both nans are the same object, so nan equals nan must be true due to equals's reflexivity requirement; +0 equals -0 is false since these two floating-point numbers have different representations. So far so good.
However, when wrapped by a generic Wrapper, == suddenly starts to generate true while comparing NaNs. I first thought that this happened due to type erasure, so it basically compares bit representations (which are equal for the same NaN), but if so, it must have printed false while comparing the wrappers of plus zero and minus zero. However, it gives true in both cases.
To make it even more puzzling, if you add an upper bound Double to the first line of this code (class Wrapper[T <: Double](val elem: T) {), it prints the following:
nan == nan: false
+0 equals -0: false
+0 == -0: true
[nan] equals [nan]: false
[nan] == [nan]: false
[+0] equals [-0]: true
[+0] == [-0]: true
So, if it bounded by Double, wrapped NaNs are not equal anymore! If you bound it by AnyVal, they are equal just as in the original code. If you make the wrapper non-generic (remove T parameter and substitute it with Double), they are unequal. So clearly there is something to do with what the compiler "remembers" about the internal structure of Doubles in different cases. But what does it exactly remember and how the dispatch of == is performed?
What I would suspect is happening is that there are two different runtime implementations of IEEE double precision that Scala can use and they have slightly different equality semantics.
double, comparison is implemented using a specific JVM instruction; +0.0 == -0.0 is true and Double.NaN == Double.NaN is falsejava.lang.Double (an Object box for double), equals is implemented by comparing the bit representations, which results in NaN comparing true and +0 not being equal to -0.In Scala, the compiler will basically use the primitive double (because it's a lot faster) whenever it has proven that it's dealing with a scala.Double (and no methods of java.lang.Object are being called, such as equals) and will use java.lang.Double when (e.g. in a generic (which hasn't been @specialized...)) it can't be sure. The Scala == method is not exactly a synonym for .equals as it has special handling for things that are implemented as primitives in the JVM.
In general, usage of .equals in Scala code should be replaced with ==.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With