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
?