Scala 3 : Equality compilation error occurring only for certain types

Viewed 134

I am trying to understand Scala3 new "Multiversal Equality" feature. I am experiencing inconsistent behavior when comparing different types.

case 1. Compare Int with String:

  val x = 1
  val y = "One"

  x == y  // gives compilation error -> "Values of types Int and String cannot be compared with == or !="
  • Compilation error even without importing scala.language.strictEquality
  • This compiles in Scala2 without any errors

case 2. Compare two case classes:

  case class Cat(catname: String)
  case class Dog(dogname: String)
  val d = Dog("Frank")
  val c = Cat("Morris")

  d == c  // false, but it compiles

I am aware of the fact that I need to import scala.language.strictEquality to enforce Multiversal equality in case2. But why is it not required in case1?

2 Answers

Notice that

case 1. summon[CanEqual[Int, String]] doesn't compile even without importing scala.language.strictEquality

case 2. summon[CanEqual[Cat, Dog]]

  • compiles without importing scala.language.strictEquality but
  • doesn't compile with such importing.

a) Instances of type class CanEqual are generated by the compiler (as well as scala.reflect.ClassTag, scala.reflect.TypeTest, scala.ValueOf, scala.deriving.Mirror.Product, scala.deriving.Mirror.Sum, scala.deriving.Mirror)

  val specialHandlers = List(
    defn.ClassTagClass        -> synthesizedClassTag,
    defn.TypeTestClass        -> synthesizedTypeTest,
    defn.CanEqualClass        -> synthesizedCanEqual,
    defn.ValueOfClass         -> synthesizedValueOf,
    defn.Mirror_ProductClass  -> synthesizedProductMirror,
    defn.Mirror_SumClass      -> synthesizedSumMirror,
    defn.MirrorClass          -> synthesizedMirror)

https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala#L489-L499

b) The thing is that the case when one of type parameters L, R of CanEqual[-L, -R] is a numeric value class (Byte, Short, Char, Int, Long, Float, Double) is handled differently:

  val synthesizedCanEqual: SpecialHandler = (formal, span) =>
    ...
    if canComparePredefined(arg1, arg2)
        || !Implicits.strictEquality && explore(validEqAnyArgs(arg1, arg2))
    ...

https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala#L147-L148

Notice that here if the answer is given by canComparePredefined then it doesn't matter whether strictEquality is switched on.

c) canComparePredefined calls

def canComparePredefinedClasses(cls1: ClassSymbol, cls2: ClassSymbol): Boolean =
  ...

  if cls1.isPrimitiveValueClass then
    if cls2.isPrimitiveValueClass then
      cls1 == cls2 || cls1.isNumericValueClass && cls2.isNumericValueClass
    else
      cmpWithBoxed(cls1, cls2)
  else if cls2.isPrimitiveValueClass then
    cmpWithBoxed(cls2, cls1)
  ...
  else
    false

https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala#L108-L114

Notice that here if one of L, R is a numeric value class then the other must be too (taking into account boxing) so that summon[CanEqual[Int, Int]], summon[CanEqual[Double, Double]], summon[CanEqual[Int, Double]] compile but summon[CanEqual[Int, String]] doesn't compile.

Universal equality only works for types without CanEqual instances already defined. As stated in Multiversal Equality:

Even though canEqualAny is not declared as given, the compiler will still construct an canEqualAny instance as answer to an implicit search for the type CanEqual[L, R], unless L or R have CanEqual instances defined on them, or the language feature strictEquality is enabled.

and since there is already an instance of CanEqual for String defined in the companion object as:

given canEqualString: CanEqual[String, String] = derived

universal equality will not work for String nor Int (as per @DmytroMitin the compiler makes special consideration to Scala's numeric types) even with strictEquality disabled.

Related