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 aCanEqual
instance 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?