Scala 3:仅针对某些类型发生的平等编译错误

Scala 3 : Equality compilation error occurring only for certain types

我正在尝试了解 Scala3 新的“多重宇宙平等”功能。我在比较不同类型时遇到不一致的行为。

案例 1. 将 Int 与 String 进行比较:

  val x = 1
  val y = "One"

  x == y  // gives compilation error -> "Values of types Int and String cannot be compared with == or !="

case 2.比较两个case 类:

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

  d == c  // false, but it compiles

我知道我需要 import scala.language.strictEquality 以在案例 2 中强制执行多元宇宙平等。但是为什么case1不需要呢?

通用相等性仅适用于尚未定义 CanEqual 个实例的类型。如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.

并且由于在伴随对象中已经为 String 定义了一个 CanEqual 实例:

given canEqualString: CanEqual[String, String] = derived

通用相等性不适用于 StringInt(根据@DmytroMitin,编译器会特别考虑 Scala 的数字类型),即使 strictEquality 已禁用。

注意

案例 1. summon[CanEqual[Int, String]] 即使不导入也无法编译 scala.language.strictEquality

案例 2。summon[CanEqual[Cat, Dog]]

  • 无需导入即可编译 scala.language.strictEquality
  • 无法通过此类导入进行编译。

a) class CanEqual 类型的实例由编译器生成(以及 scala.reflect.ClassTagscala.reflect.TypeTestscala.ValueOfscala.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) 问题是 CanEqual[-L, -R] 的类型参数 L, R 之一是数值 class (Byte , Short, Char, Int, Long, Float, Double)的处理方式不同:

  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

注意,这里如果答案是由canComparePredefined给出的,那么strictEquality是否打开并不重要。

c) canComparePredefined 调用

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

请注意,如果 LR 中的一个是数值 class,那么另一个也必须是数值(考虑到装箱),这样 summon[CanEqual[Int, Int]] , summon[CanEqual[Double, Double]], summon[CanEqual[Int, Double]] 编译但 summon[CanEqual[Int, String]] 不编译。