So here’s a proposed tweak that keeps @odersky’s .checkedCast proposal mostly in place, but aims to make it more “well behaved” as a Scala “method” (names are all arbitrary and can be discussed later):
checkedCast is defined in the standard library as below (it could also be an extension method or static method)
trait Any{
def checkedCast: CheckedCast[this.type] = this.asInstanceOf[CheckedCast[this.type]]
}
scala.CheckedCast[T] is a “magic” opaque type alias defined in the standard lib
opaque type CheckedCast[T] = T
scala.CheckedCast[T] is handled specially by the compiler. It has no members, and the only thing you can do to it is match or it or assign it to things. match or assigning a CheckedCast[T] treats it as a T, but disables the exhaustivity checking (this is the necessary compiler magic)
AFAICT, this should keep most of @odersky’s examples working, but also make all the cases that @sjrd and I brought up work
{ val y: x.type = x; y.checkedCast } match { ... }
This will work because we will no longer be looking syntactically at a trailing .checkedCast method, and would instead have the y.checkedCast return CheckCast[x.type] which would propagate out of the block and thus reach the match
val y: x.type = x.checkedCast
y match { ... }
This would now work because instead of val y: x.type, we would now have val y: CheckedCast[x.type], which would then reach the match
def f(y: CheckedCast[Foo]) = y match ...
f(x.uncheckedCast)
This would now work because CheckedCast[T] is a proper type, and we can have values of that type, parameters of that type, and it allows the x.uncheckedCast’s CheckedCast[Foo] propagate into f as the parameter y, to then reach the match
def foo[T](x: T) = x.checkedCast
A forwarder/alias foo can now be defined, with the type being foo[T](x: T): UncheckedCast[T]. You can now do foo(x) match and the .uncheckedCast inside the foo method will propagate out out of the foo to reach the match outside of it
identity(foo.checkedCast) match
This might work, because identity would infer the type parameter UncheckedCast[foo.type], which would propagate and then reach the match
Seq(foo, bar).map(_.checkedCast: TargetType).map{ ...partial function }
This would now work, where TargetType is written CheckedCast[T]. It would propagate into the parameter of map’s lambda, and allow the partial function passed to map to be partial
val x: TargetType = foo.checkedCast; x match
This would now work. TargetType would be spelled CheckedCast[foo.type]
foo.checkedCast[TargetType] match
This would work, as checkedCast is a normal method. It would not take a type parameter, so it would just be foo.checkedCast match
val x = foo.checkedCast: TargetType; x match
This would work, with TargetType being spelled CheckedCast[foo.type]
Overall, I think this would be a strict improvement over the original proposal. We still have some magic, but the magic is much more well defined:
-
A single generic opaque type alias which is handled specially during match and =.
-
No more fragile pattern matching on the exact shape of the syntax tree
-
.checkedCast becomes a normal method defined entirely in user-land that behaves identically to all other methods in all regards.
-
Even the type CheckedCast[T] behaves exactly like any other type, and can be written out in type ascriptions, inferred, used in generics (e.g. Seq[CheckedCast[T]]), etc. up until it reaches a match or = at which point the magic kicks in in a very localized fashion
I’m still not convinced that .checkedCast should look like a method. In all the examples given, it looks like it should be a keyword modifier on val or match. However, if we do assume that it should look like a method, these tweaks would ensure that it behaves like a method for most intents and purposes, except for a tiny bit of compiler magic to disable exhaustivity checking in match and val when the expression is of type CheckedCast[T]