Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does a generic wrapper make NaNs equal in Scala?

Tags:

nan

scala

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?

like image 762
Wolfram Avatar asked Nov 18 '25 10:11

Wolfram


1 Answers

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.

  • For the Java primitive double, comparison is implemented using a specific JVM instruction; +0.0 == -0.0 is true and Double.NaN == Double.NaN is false
  • For java.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 ==.

like image 127
Levi Ramsey Avatar answered Nov 21 '25 00:11

Levi Ramsey