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
-- 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.
@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] =>
@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.
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
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 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.
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.
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
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.
We could also consider other names instead of
checkedCast. Maybe simply