I don't understand which generic type parameters Scala erases. I used to think that it should erase all generic type parameters, but this does not seem to be the case.
Correct me if I'm wrong: if I instantiate an instance of type Map[Int, String] in the code, then at the runtime, the instance knows only that it is of type Map[_, _], and does not know anything about its generic type parameters. This is why the following succeeds to compile and to execute without errors:
val x: Map[Int, String] = Map(2 -> "a")
val y: Map[String, Int] = x.asInstanceOf[Map[String, Int]]
Now I would expect that all higher-kinded type parameters are also erased, that is, if I have a
class Foo[H[_, _], X, Y]
I would expect that an instance of type Foo[Map, Int, String] knows nothing about Map. But now consider the following sequence of type-cast "experiments":
import scala.language.higherKinds
def cast1[A](a: Any): A = a.asInstanceOf[A]
def cast2[H[_, _], A, B](a: Any) = a.asInstanceOf[H[A, B]]
def cast3[F[_[_, _], _, _], H[_, _], X, Y](a: Any) = a.asInstanceOf[F[H, X, Y]]
class CastTo[H[_, _], A, B] {
def cast(x: Any): H[A, B] = x.asInstanceOf[H[A, B]]
}
ignoreException {
val v1 = cast1[String](List[Int](1, 2, 3))
// throws ClassCastException
}
ignoreException {
val v2 = cast2[Map, Int, Long](Map[String, Double]("a" -> 1.0))
// doesn't complain at all, `A` and `B` erased
}
ignoreException {
// right kind
val v3 = cast2[Map, Int, Long]((x: Int) => x.toLong)
// throws ClassCastException
}
ignoreException {
// wrong kind
val v4 = cast2[Map, Int, Long]("wrong kind")
// throws ClassCastException
}
ignoreException {
class Foo[H[_, _], X, Y](h: H[X, Y])
val x = new Foo[Function, Int, String](n => "#" * n)
val v5 = cast3[Foo, Map, Int, Long](x)
// nothing happens, happily replaces `Function` by `Map`
}
ignoreException {
val v6 = (new CastTo[Map, Int, Long]).cast(List("hello?"))
// throws ClassCastException
}
ignoreException {
val castToMap = new CastTo[Map, Int, Long]
val v7 = castToMap.cast("how can it detect this?")
// throws ClassCastException
}
ignoreException {
val castToMap = new CastTo[Map, Int, Long]
val madCast = castToMap.asInstanceOf[CastTo[Function, Float, Double]]
val v8 = madCast.cast("what does it detect at all?")
// String cannot be cast to Function???
// Why does it retain any information about `Function` here?
}
// --------------------------------------------------------------------
var ignoreBlockCounter = 0
/** Executes piece of code,
* catches an exeption (if one is thrown),
* prints number of `ignoreException`-wrapped block,
* prints name of the exception.
*/
def ignoreException[U](f: => U): Unit = {
ignoreBlockCounter += 1
try {
f
} catch {
case e: Exception =>
println("[" + ignoreBlockCounter + "]" + e)
}
}
Here is the output (scala -version 2.12.4):
[1]java.lang.ClassCastException: scala.collection.immutable.$colon$colon cannot be cast to java.lang.String
[3]java.lang.ClassCastException: Main$$anon$1$$Lambda$143/1744347043 cannot be cast to scala.collection.immutable.Map
[4]java.lang.ClassCastException: java.lang.String cannot be cast to scala.collection.immutable.Map
[6]java.lang.ClassCastException: scala.collection.immutable.$colon$colon cannot be cast to scala.collection.immutable.Map
[7]java.lang.ClassCastException: java.lang.String cannot be cast to scala.collection.immutable.Map
[8]java.lang.ClassCastException: java.lang.String cannot be cast to scala.Function1
asInstanceOf[Foo[...]] does care about Foo, this is expected.asInstanceOf[Foo[X,Y]] does not care about X and Y, this is also expected.asInstanceOf does not care about higher kinded type parameter Map, similar to case 2, this is also expected.So far so good. However, the cases 6, 7, 8 suggest a different behavior: here, an instance of type CastTo[Foo, X, Y] seems to retain information about the generic type parameter Foo for some reason. More precisely, a CastTo[Map, Int, Long] seems to carry around enough information with it to know that a string cannot be cast into a Map. Moreover, in case 8, it seems to even change Map to Function because of a cast.
Question(s):
CastTo is not erased, or is there something else what I don't see? Some implicit operation or anything? Thanks for reading.
EDIT: Poking around in similar examples revealed an issue with the 2.12.4-compiler (see my own "answer" below), but this is a separate issue.
I think you are confusing some things.
Casts to generic types are deferred until the point where the types become concrete. For example, take this piece of code:
class CastTo[H[_, _], A, B] {
def cast(x: Any): H[A, B] = x.asInstanceOf[H[A, B]]
}
In the bytecode you can only cast to a real class, because it doesn't know anything about generics. So the above will, in bytecode, be roughly equivalent to:
class CastTo {
def cast(x: Object): Object = x
}
Then later in the code you give a String to method cast and the compiler can see that according to the type information it has, a Map[Int, Long] will come out. But in the bytecode cast has an erased return type of Object, so the compiler has to insert a cast at the use-site of the cast method. This code
val castToMap = new CastTo[Map, Int, Long]
val v7 = castToMap.cast("how can it detect this?")
will, in the bytecode, be roughly equivalent to the following (pseudo) code:
val castToMap = new CastTo
val v7 = castToMap.cast("how can it detect this?").asInstanceOf[Map]
As for your other questions:
String to a Map[Int, Long]. That is bound to crash eventually. Failing (relatively) fast with a ClassCastException is probably the safest, most user-friendly option.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With