What is the reason enumeration does not derive CanEqual?
It was very surprising that this is not the default case, which led me to submit the following issue:
However, only after I noticed that the test cases explicitly check the interaction of enums and strictEquality, I understood that this is an intended design decision.
I started experimenting with strictEquality just recently. Once it’s on it pretty much requires you to have it specified on every case class that will participate in pattern matching.
IME so far it’s strictly worse than Scala 2’s linting support + -Xfatal-warnings.
In Scala 2 you do literally nothing and get actionable errors for the cases you care about (i.e. where you are mistakenly comparing values of different types).
In Scala 3 you have to pepper derives CanEqual on every non-primitive type where you’ll be doing equality checking.
For example, here are some ScalaTest failures that have me considering giving up and disabling strictEquality in sbt test scope.
assert(x == Some(model))
Values of types Option[User] and Some[User] cannot be compared with == or !=
assert(x == List(model))
Values of types List[User] and List[User] cannot be compared with == or !=
Compared to Scala 2 it’s kind of like the tail wagging the dog, but I guess this is the only solution until linting support lands in Scala 3.
I feel the same way. I really believe CanEqual should be derived automatically for reasonable structures, like case class, sealed trait, enum. Until then I hardly imagine some uses this feature.
For anyone else stumbling over this, here are two givens covering the aforementioned things like case class, sealed trait and enum:
inline given canEqualProductOrSum[T](using m: Mirror.Of[T]): CanEqual[T, T] =
summonInline[CanEqual[m.MirroredElemTypes, m.MirroredElemTypes]].asInstanceOf[CanEqual[T, T]]
given canEqualSingleton[T <: Singleton]: CanEqual[T, T] = CanEqual.derived
Imho they should be in the standard library.
cats users might also want to add
given canEqualCatsEq[T](using cats.kernel.Eq[T]): CanEqual[T, T] = CanEqual.derived
If you have AnyVals: (Note that one could define an AnyVal with underlying objects that are not comparable, e.g. class F(Int => Int) extends AnyVal. If you might be tempted to do this, don’t use the following one.)
given canEqualAnyVal[T <: AnyVal]: CanEqual[T, T] = CanEqual.derived
If you only want case classes, but not sealed traits or enums, replace the Mirror.Of by Mirror.ProductOf.
If you only want enums, you may use
given canEqualEnum[T <: scala.reflect.Enum]: CanEqual[T, T] = CanEqual.derived
Thank you for this proposal. But I would like to push back a bit. IME, equality testing using == is actually not super common for ADTs. The real issue is pattern matching, as @SimY4 has pointed out:
(I think he meant to say enums there, because pattern matching on case classes doesn’t require CanEqual). Pattern matching is what my improvement proposal, SIP-67, solves. It was accepted a few months ago and I have submitted an implementation in the compiler yesterday.
While it’s tempting to solve the problem by providing a magic CanEqual instance, it doesn’t work for several important cases.
It doesn’t work for recursive enums like enum List[+A] { case Nil; case Cons(head: A, tail: List[A]) }
More problematically, it doesn’t work for enums where any of the cases contain a type that doesn’t have CanEqual, like for example a function type: enum Foo { case Bar; case Baz(f: Int => Int) }. A crucial point here is that this type not having CanEqual is actually a feature, not a bug: there is no way to determine if two functions are the same, and hence CanEqualshouldn’t be available – but we still need to be able to pattern match on this ADT.
I’m also generally opposed to “fully automatic derivation” (as opposed to semi-automatic derivation like you get with derives XXX clauses).
Letting the author of an enum type control whether or not a typeclass instance is available is a feature, not a bug. It can be a conscious decision to omit a derives clause, for example because the author of the ADT anticipates adding more cases in the future involving types that don’t have the required typeclass instances
fully-automatic derivation failures are an absolute nightmare to debug. When you enforce derives clauses for all typeclass instances, ADT authors are immediately notified when they make a change that causes derivation to fail. With fully-automatic derivation, it’s the user of the ADT who gets the error message rather than the author. Keep in mind that ADTs might be defined in a library, so somebody might make a simple library update which causes auto-derivation to fail. And because auto-derivation is recursive, this can happen many levels away from the root cause of the problem.
fully-automatic derivation leads to code bloat and slow compile times, because the same derivation will happen everywhere you use the typeclass, instead of just once as it does with a derives clause.
For all these reasons, I’m opposed to any automatic derivations in the standard library. == seems like too common a thing to require a derives clause for at first glance, but I suspect most derives CanEqual clauses on enum types actually exist only to enable pattern matching. For the remaining cases, sprinkling a few derives CanEqual clauses over the code base here and there is not a big deal – IMO a much better option than accepting all of the downsides of fully-automatic derivation that I have laid out above. Fully-automatic derivation is one of those ideas that are super tempting at first but turn out to be a bad idea in practice in the long term.