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]