scala3 icon indicating copy to clipboard operation
scala3 copied to clipboard

Documentation help needed: `given [T]: CanEqual[T, T]` resolves `CanEqual`s of different types

Open joan38 opened this issue 1 year ago • 10 comments

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

joan38 avatar Oct 02 '23 05:10 joan38

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].

rayrobdod avatar Oct 02 '23 05:10 rayrobdod

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.

som-snytt avatar Oct 02 '23 05:10 som-snytt

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])
    ()
  }

bishabosha avatar Oct 02 '23 07:10 bishabosha

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

bishabosha avatar Oct 02 '23 07:10 bishabosha

I'd say this is by design because inference can always widen (EDIT: a contravariant type parameter) to make a constraint work

bishabosha avatar Oct 02 '23 07:10 bishabosha

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.

joan38 avatar Oct 02 '23 19:10 joan38

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

rayrobdod avatar Oct 03 '23 02:10 rayrobdod

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].

rayrobdod avatar Oct 03 '23 02:10 rayrobdod

Perhaps we should add a recommendation in the Docs (API as well) to not define a "universal" CanEqual

bishabosha avatar Oct 03 '23 08:10 bishabosha

The commit references are spurious typos. (The test name in that commit is also a typo.)

som-snytt avatar May 02 '24 19:05 som-snytt