scala3
scala3 copied to clipboard
Documentation help needed: `given [T]: CanEqual[T, T]` resolves `CanEqual`s of different types
Compiler version: 3.3.1
Compiler option: -language:strictEquality
The following code compiles fine. But why? Where does it finds CanEqual[Test, Test2]
of different types?
case class Test(i: Int)
case class Test2(i: Int)
object Eq:
given [T]: CanEqual[T, T] = CanEqual.derived
@main def run(): Unit =
Test(1) == Test2(2)
()
https://scastie.scala-lang.org/X6boEpObQXee5uhESbviuQ
Thanks
I believe that the generic given is allowing scala to find a CanEqual[Test1 | Test2, Test1 | Test2]
, which due to CanEqual's variance, satisfies the need for a CanEqual[Test1, Test2]
.
for contrast
given [T <: Product]: CanEqual[T, T] = CanEqual.derived
@main def run(): Unit = Test(1) == Test2(2) && Test(1) == 42
I assumed it was taking T
as Any
or similar, but I'm not sure what compiler options tell me the debug.
if I add this extra method:
def doEqual[T1, T2](t1: T1, t2: T2)(using CanEqual[T1, T2]) = t1 == t2
it becomes clear that doEqual(Test(1), Test2(2))
infers T
to be Test1 | Test2
, using the -Xprint:typer
flag to debug:
@main def run(): Unit =
{
Eq.doEqual[Test, Test2](Test.apply(1), Test2.apply(2))(
Eq.given_CanEqual_T_T[Test | Test2])
()
}
I believe this changed since https://github.com/lampepfl/dotty/pull/15642, but might be wrong.
However even in 3.0.0 this still compiled, just with Object
inferred as the argument
I'd say this is by design because inference can always widen (EDIT: a contravariant type parameter) to make a constraint work
Why is this "by design" only for given CanEqual
?
Do we now have different behavior depending on which given
we are talking about?
For example if I define my own equals, now the same rules don't apply:
case class Test(i: Int)
case class Test2(i: Int)
trait MyEq[A, B]
object Eq:
given [T]: MyEq[T, T] = ???
def myEquals[A, B](a: A, b: B)(using myEq: MyEq[A, B]) = ???
@main def run(): Unit =
myEquals(Test(1), Test2(2))
()
https://scastie.scala-lang.org/gCcmi7hgQBqu6eN9XlIySA
That does not sound ok to me.
Eq
is contravariant. If MyEq
is made contravariant, then the same rules will apply to MyEq
as to Eq
: https://scastie.scala-lang.org/BIbZaKwFR7ihbTQGl7lJCg
Otherwise, you end up with strict equals being too strict, and not being able to compare things that should be comparable.
Like how in munit, which tries to have a strict equals with def assertEquals[A, B](a: A, b: B)(implicit ev: B <:< A): Boolean
, has a compilation error for assertEquals(Nil, value:List[Int])
because it is not the case that Nil.type <:< List[Int]
, even though it makes sense to be able to compare two lists even if one of the lists can be determined to be a Nil.type
and not just a List[Int]
.
Perhaps we should add a recommendation in the Docs (API as well) to not define a "universal" CanEqual
The commit references are spurious typos. (The test name in that commit is also a typo.)