UPDATE: ignore the asInstanceOf clutter in this message – it was a glitch of mind, sorry for the hustle! However, the issue with the “match may not be exhaustive” warning seems real. For more accurate code example feel free to follow to this reply down below. Thanks!
Recently I came across a snippet that was looking confusing to me. Turns out there are two separate issues in there. A simplified example:
// --- uncomment this:
final class Foo[A] { // can be non-final just as well
def printMe(a: A): Unit = println(s"$a [${a.getClass.getSimpleName }]")
}
def Foo[A]: Foo[A] = new Foo[A]
// --- or this:
// abstract sealed class Foo[A] {
// def printMe(a: A): Unit = println(s"$a [${a.getClass.getSimpleName }]")
// }
// def Foo[A]: Foo[A] = new Foo[A] {}
// --- END
case class Bar[A](foo: Foo[A], a: A) {
def printIt: Unit = foo.printMe(a)
}
def Bar(t: (Foo[?], Either[Any, Any])): Bar[?] =
// When Foo is a concrete class, this match triggers "may not be exhaustive" warning but on Scala 2.{12/13} only.
// When Foo is sealed abstract, then the warning is not triggered.
// On Scala3 (3.3+) no warnings in any case.
t match {
// These `asInstanceOf` below do nothing.
// No "unchecked" warning or something whatsoever!
case (foo: Foo[ta], Left(va)) => Bar(foo, va.asInstanceOf[ta])
case (foo: Foo[ta], Right(va)) => Bar(foo, va.asInstanceOf[ta])
}
// Completes without any exception.
Bar((Foo[Int], Right("abc"))).printIt
t match triggers “may not be exhaustive” warning, but only if:
Foo is a concrete class (either final or not, or abstract, but notsealed)
Compiled with Scala 2 (both 2.12 and 2.13). Scala 3 generates no warnings whatsoever.
In fact, there is no non-exhaustive matching – all the possible cases are covered. However:
va.asInstanceOf[ta] does nothing here – it simply pacifiers compiler and makes it believe that va has ta type. Perhaps, it is OK in that case, but I would expect to see some sort of “unchecked” warning in there. Neither Scala2 nor Scala3 emits such a warning (even with the “-unchecked” compiler option).
So the behaviour of all the compilers I tried is confusing at least (or even misleading, to some extent).
Well, what I meant, actually, is that there’s usually a runtime check involved:
scala> def foo[A](a: Any): A = a.asInstanceOf[A]
def foo[A](a: Any): A
scala> foo[String](123)
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
... 32 elided
But you’re right – it has nothing to do with the compiler itself.
What stumbled me, actually, is that when I have matcing on generic types that cannot be checked, I expect to get something like this:
scala> def foo[A](a: Any): A = a match { case aa: A => aa }
1 warning found
-- [E092] Pattern Match Unchecked Warning: -------------------------------------
1 |def foo[A](a: Any): A = a match { case aa: A => aa }
| ^
|the type test for A cannot be checked at runtime because it refers to an abstract type member or type parameter
|
| longer explanation available when compiling with `-explain`
def foo[A](a: Any): A
And in the above example there is a pretty unusual matching with type capturing, but no “unchecked” warnings. Instead I got “may not be exhaustive” one, which it is not.
So I guess the concern regarding asInstanceOf can be considered as cleared out. But the other one regarding non-exhaustive matching still in place, I guess.
scala> println(foo[String](123))
123
scala> val result: AnyRef = foo[String](123)
val result: AnyRef = 123
As long as the types are generic there can be no runtime check. It’s only when you try to use the result of foo[String](123) as a String that you’ll get a ClassCastException (e.g. when the REPL automatically assigns it to a variable of type String).
In this piece of code there’s a type test with a generic type that cannot be checked at runtime so you get an “unchecked” warning. As you say in your first example there was “type capturing”. That’s something else and does not need to be checked at runtime, so there’s no warning.
The exhaustivity warning is probably a bug though.
In attempt to single out the “match may not be exhaustive” warning, I ended up with a simpler and more precise example:
// --- uncomment this:
final class Foo[A] {}
def foo[A] = new Foo[A]
// --- or this:
//abstract sealed class Foo[A] {}
//def foo[A] = new Foo[A] {}
// --- END
def alwaysOk1(foos: List[Foo[Any]]): Unit = foos match {
case (foo: Foo[?]) :: _ => ()
case Nil => ()
}
def warnsForNonSealedOnly1(foos: List[Foo[?]]): Unit = foos match {
case (foo: Foo[?]) :: _ => ()
case Nil => ()
}
def alwaysOk2(foo: Option[Foo[Any]]): Unit = foo match {
case Some(foo: Foo[?]) => ()
case None => ()
}
def warnsForNonSealedOnly2(foo: Option[Foo[?]]): Unit = foo match {
case Some(foo: Foo[?]) => ()
case None => ()
}
def alwaysOk3(t: ((Int, (Foo[?], String)), Double)): Unit = t match {
case ((a, (foo: Foo[?], b)), c) => ()
}
I.e. the warning emerges on Scala 2 only (both 2.13 and 2.12) and only if all three conditions are met:
Foo is a non-sealed class. It can be either abstract or concrete though – it doesn’t matter.
Foo is passed with a whildcard ?, not Any.
There’s a switch between two or more cases involved in the match expression, not just extraction of values or types.
The presense of type extraction doesn’t matter, actually – type whildcards lead to the same result.
I wonder if it makes sense to report it to scala/bug? I see there are a lot of other issues regarding false “match may not be exhaustive” warning, but couldn’t find one that would have sealed/non-sealed dichotomy involved.