The Problem
There are ongoing efforts to tighten up pattern matching to make refutable patterns static errors. For instance, consider:
def xs: List[Any] = ???
val y :: ys = xs
This compiled without warning in 3.0, became a warning in 3.2, and we’d like to make it an error in one of the next 3.x versions. As an escape hatch we recommend to use @unchecked:
-- Warning: ../../new/test.scala:6:16 ------------------------------------------
6 | val y :: ys = xs
| ^^
|pattern's type ::[Any] is more specialized than the right hand side expression's type List[Any]
|
|If the narrowing is intentional, this can be communicated by adding `: @unchecked` after the expression,
|which may result in a MatchError at runtime.
Similarly for non-exhaustive matches, where we also recommend to put @unchecked on the scrutinee.
But @unchecked has several problems. First, it is ergonomically bad. For instance to fix the exhaustivity warning in
xs match
case y :: ys => ...
we’d have to write
(xs: @unchecked) match
case y :: ys => ...
Having to wrap the @unchecked in parentheses feels clunky and unnatural.
Second, and more importantly, the word unchecked is a misnomer. After all, the pattern is checked, but it is done at runtime. This is particularly bad since there is another usage of @unchecked which means the exact opposite:
xs match
case _: List[Int @unchecked] =>
Here, @unchecked means not checked at runtime: The compiler trusts the user that this a List[int]; it might insert casts to Int but not at the place where the pattern is matched.
The Proposal
I propose to fix both problems by deprecating @unchecked in the usage where it means “checked at runtime” and replacing it with a new magic method on Any, named checkedCast, or something similar.
So the previous examples would become:
def xs: List[Any] = ???
val y :: ys = xs.checkedCast
xs.checkedCast match
case y :: ys => ...
The Spec
The rules for checkedCast would be similar to the current @unchecked annotation on selectors. checkedCast could be a synthetic method on Any defined like this:
def checkedCast: this.type = this
Then when it comes to exhaustivity or refutability checking, a scrutinee ending with .checkedCast would silence all warnings or errors. Furthermore, .checkedCast calls would be removed after
the pattern matching translation.
A Possible Refinement
One tricky aspect of val definitions is this:
val x: Any = ???
val (y: Int) = x: @unchecked // works
val y: Int = x: @unchecked // gives a type error: found: `Any`, expected: `Int`
Line 2 is syntactically a pattern match, where we allow refutable patterns, so the code typechecks.
By contrast line 3 is a val definition, where we do not allow this. I believe with checkedCast we don’t need such hair splitting anymore. So we could allow both of the following two definitions:
val (y: Int) = x.checkedCast
val y: Int = x.checkedCast
The general rule would be that checkedCast can also heal type errors. In an expression
e.checkedCast, if the type of e does not conform to the expected type XT, translate the expression to
e.checkedCast match
case $x: XT => $x
and proceed as before.
Why not use asInstanceOf instead?
asInstanceOf is a low-level cast which might not necessarily check missing cases. Furthermore, one cannot in general simply leave out the type argument of an asInstanceOf. if you write x.asInstanceOf without a type argument this will often work, but sometimes the missing type argument will be inferred to Nothing in which case you get a runtime error since x cannot be cast to Nothing. This is typically if the expected type of the asInstanceOf is underdefined. By contrast, an underdefined expected type would lead to a warning or error in the match expansion of checkedCast.
Why not use a new method based on TypeTest instead?
Essentially, simplicity. One method is easier to remember than two. And they do morally the same thing. Also, it might be hard to encode the precise meaning of “case x: XT is a pattern match that does not produce errors or warnings” into a testable type.
Alternative names
We could also consider other names instead of checkedCast. Maybe simply checked, or narrowed?