If T
is a parameterized type, then isInstanceOf[T]
must give an erasure warning, even if we have a ClassTag[T]
in scope.
Alternatively, we could ensure that ClassTag[T]
does not exist for parameterized types, but only for their wildcard versions. More alternatives might be desirable, since ClassTag[T]
is also used for array creation and that has different requirements.
This code (files below) demonstrates why.
The issue is that we get heap pollution and a ClassCastException
without casts or erasure warnings, and that must not happen, as described by Java's guarantees for erasure warnings, which I expect should apply to us too:
https://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html
Googling instanceof classtag unsound finds nothing, but somebody must have noticed? I expect I never did, but this issue is surprisingly easy to trigger. Apparently shapeless's Typeable takes care of avoiding the issue: https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/typeable.scala
The proposals below were collected from Gitter, with some added comments. This summary has no pretense to be complete.
Scala compiler treat ClassTag
specially, so adding further special cases might be acceptable. Designing a more general API is hard.
Downside: using ClassTag[List[Int]]
to create an array is fully sound, and would now require a cast on the resulting array even though the compiler knows that cast can't fail.
I imagined a special case. @Ichoran on gitter suggests defining a generic type function Erasure
such that Erasure[F[A]]
was F[_]
, and using it as implicit def createClassTag[T]: ClassTag[Erasure[T]]
. I like that, given at least another good usecase.
For a contrived example, Erasure
would allow giving better types to (variants of) safeCast
, such as safeCastAs
:
type BetterClassTag[T] = ClassTag[Erasure[T]]
def safeCastAs[T: BetterClassTag](x: Any, y: T): Option[(Erasure[T], T)] = {
x match {
case x: T => Some(x)
case _ => None
}
}
However, @alexknvl remarks that Erasure
would be an unusual type function in Scala, since it's neither constant nor injective and it "pattern-matches" into its argument so it's not "parametric" in some sense. (I don't think it's the first, but all the ones I can find require complex encodings). Non-injective type constructors are also problematic in Hindley-Milner type inference (as well-known from Haskell type families); even in Scala, passing an Erasure[B]
to a function demanding Erasure[A]
doesn't let the compiler infer that A = B
.
But these downsides might be smaller than later proposals, right now (iterations pending).
Possibly, this API should be combined with something else so that inference problems can be avoided by coding patterns.
Use ErasedClassTag[A]
for matching and ClassTag[A]
for array creation. But taking ErasedClassTag[A]
and ClassTag[A]
for related types is still hard, it seems you'd need cta: ClassTag[A], ectea: ErasedClassTag[Erasure[A]]
.
def foo[T @erasedType: ClassTag, U: ClassTag](x: Any) = {
...
x.isInstanceOf[T] //no warning
x.isInstanceOf[U] //warning
...
}
Still can't relate two type arguments except through Erasure
.
(Not erased
because it's a Dotty keyword).
An alternative would be a type member in ClassTag
.
object stdlib {
// I don't propose this annotation, so private.
private sealed trait erasedType extends scala.annotation.Annotation
// I'm not sure I propose this type, but I can't make it private.
type Erasure[T]
// This is the API I'm looking at, except it has to be special and I can't express how it is special in pure Scala, hence the above
// ("private") APIs.
trait MyClassTag[T] {
type Erased
}
object MyClassTag {
//I propose to restrict this method to erased T arguments.
//implicit def myClassTag[@erasedType T <: AnyRef]: MyClassTag[T] { type Erased = T } = ???
implicit def myClassTag[T <: AnyRef]: MyClassTag[T] { type Erased = Erasure[T] } = ???
}
implicit def pathDependentImplicitly[T](implicit x: T): x.type = x
val ctListInt = pathDependentImplicitly[MyClassTag[List[Int]]]
def f(xs: List[_]): ctListInt.Erased = xs
}
One annoying thing is that type members only work on stable terms, and among implicits only val
s are stable; the above code requires pathDependentImplicitly
instead of implicitly
, and pathDependentImplicitly[MyClassTag[List[Int]]].Erased
would also fail.
Consider also what's below:
val t: ClassTag[A] = A // here t.Erased is unconstrained, hence as alternative
val t = A // no type annotation, can trigger warnings
val t: ClassTag[A] { type Erased = A.Erased } = A // requires `Erased` member on all types and type constructors
val t: ClassTag[A] { type Erased = ErasedA } = A //require carrying both A and ErasedA, and it's unclear how to force them to be related
Uh, a possible solution would be that the argument for
T: ClassTag
should not beList[String]
butList[_]
.