scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.72k stars 1.04k forks source link

Explicit type specification in `match` worsens type checks #20705

Open mohe2015 opened 2 weeks ago

mohe2015 commented 2 weeks ago

Compiler version

3.3.3

Minimized code

@main
def main(): Unit = {
  val example: Tuple2[Int | String, Int | String] = ("test", 2)
  example match {
    case newExample@Tuple2(a: Int, b: Int) => println("hi")
    case other => println("unmatched")
  }
}

prints "unmatched" but

@main
def main(): Unit = {
  val example: Tuple2[Int | String, Int | String] = ("test", 2)
  example match {
    case newExample@Tuple2[Int, Int](a: Int, b: Int) => println("hi")
    case other => println("unmatched")
  }
}

throws a ClassCastException.

Expectation

While it's understandable that the compiler can't infer the types correctly (this seems to be https://github.com/scala/scala3/issues/15972), I think it's unexpected that just adding a type specifier removes the runtime type checks.

LucySMartin commented 2 weeks ago

I don't nessaserilly agree with the assessment that this is removing an additional check - its simply erroring before you get to the check that you have in both.

If we break down the logic a bit more, in the first example this is trying to break down example into a tuple, and then attempting to pattern match inside of it.

In the second example, a and b do not exist unless you have already successfully run extractors on a Tuple2[Int, Int] - which is going to fail.

Imagine a case where the inner extractor was not a simple type check, but passing through to some custom unapply method, which accepted only positive Ints, and ascii strings. In this hypothetical, the user in the first case would be asking "Do I have two fields which are either posative ints or ascii strings" and in the second is asking "Do I have exactly two posative ints".

We could also consider examples which were more complex than a simple tuple. For example class Foo[T](foo: Int | T). In this case an extractor of Foo[Int](a: int) is clearly stronger than Foo(a:Int) - and it would be up to the implementation of Foo.unapply to determine how this further refinement was handled. In the case of case classes (and thus tuples) - unapply promises to preserve known types - and thus has to fail when the types is wrong.

If we wanted to make this work it would be opening up a whole heap of things in constructs like val Tuple2[Int,Int](a, b) = (???,???) with a and b not getting the fully validated type.