Having been bitten by univeral equality again recently, I once again considered switching on the -language:strictEquality option. Alas, it’s not currently a great experience.
For me, the major issue with it is pattern matching against case objects. When you match against a case object, it performs an equality check, and unless you have a CanEqual instance for that type, this won’t compile even for trivial examples.
enum Foo:
case Bar
def f(x: Foo) =
x match
case Foo.Bar => ()
// error: Values of types Foo and Foo cannot be compared with == or !=
One could argue that it’s not so bad to just slap derives CanEqual onto that ADT and move on, but that’s unsatisfactory:
- I may not control this ADT. It might come from a library, or it could be generated by a tool. You can provide an orphan instance, but that sucks too.
- The type may include data that cannot be compared for equality, like
enum Foo { case Bar; case Baz(f: String => String) }. This type shouldn’t have aCanEqualinstance because we cannot determine equality of functions. But we still need to pattern match on it!
There are workarounds, for example, this compiles:
enum Foo:
case Bar()
def f(x: Foo) =
x match
case Foo.Bar() => ()
As does this:
enum Foo:
case Bar
def f(x: Foo) =
x match
case _: Foo.Bar.type => ()
But the code that we started out with is actually perfectly fine, and having people rewrite their code in this way (which is clearly much uglier, especially the second one) just isn’t cool, and we need a better solution than that.
At this point the obvious question is: why does it perform an equality check at all? Why not just make case Foo.Bar equivalent to case _: Foo.Bar.type? Unfortunately, that isn’t backward compatible because equals can be overridden, which means that things like this work:
Vector.empty match
case Nil => ()
Vector.empty is not Nil (because that is a List, not a Vector), but they do compare equal, which is why the pattern match succeeds.
I think the solution to this problem is to find a subset of pattern matches that we
can statically determine to be well-behaved, and allow pattern matching against case objects even when a CanEqual instance is not available.
One case that is definitely safe is when the scrutinee is of an enum type, and that enum doesn’t override the equals method, and doesn’t extend any other type (that might override the equals method), and the pattern is one of the cases of the enum.
What other cases should be allowed? What should the rules be here?