Question 1:
The JVM does not know about generics, therefore type parameters in Scala (and Java) only exist at compile-time. They don't exist at run-time. Since Akka is a Scala (and Java) framework, it suffers from this very shortcoming, too. It suffers from it in particular because in Akka, messages between actors are (obviously) only exchanged during run-time, so all the type arguments of those very messages are lost. Correct so far?
Question 2:
Say I defined the following case class that takes one type parameter:
case class Event[T](t: T)
Now, I instantiate an Event[Int](42)
and send it to my testActor
. Is it correct that my testActor
basically receives an Event[Any]
and has no idea of what type t
is?
Question 3:
Say, inside my testActor
there exists a function that also takes a type parameter:
def f[T](t: T) = println(t)
The testActor
calls f
when receiving an Event
:
override def receive: Receive = {
case Event(t) => f(t)
}
To what will the type parameter T
of f
be set when the function is called like this? Any
? If so, would the following function effectively be equivalent to the above (assuming it would only get called like described above):
def f2(t: Any) = println(t)
Question 4:
Now, consider this definition of f
:
def f[T](t: T) = println(t.getClass)
I didn't change the call-site:
override def receive: Receive = {
case Event(t) => f(t)
}
Shouldn't this always print Any
to the console? When I send an Event[Int](42)
to my testActor
, it does print java.lang.Integer
to the console, though. So the type information isn't erased after all? I am confused.
Calling type erasure a "shortcoming" seems kind of like begging the question, but whatever, this paragraph sounds fairly reasonable to me, maybe with some quibbles about manifests and class tags and what "exists" means. :)
Not exactly. Consider the following similar case class and method:
case class Foo[T](v: T, f: T => Int)
def doSomething(x: Any): Unit = x match {
case Foo(v, f) => println(f(v))
case _ => println("whatever")
}
This works just fine:
scala> doSomething(Foo("hello world", (_: String).size))
11
So we're not just seeing the Foo
as a Foo[Any]
, since the (_: String).size
is not a valid Any => Int
:
scala> val stringSize: Any => Int = (_: String).size
<console>:11: error: type mismatch;
found : String => Int
required: Any => Int
val stringSize: Any => Int = (_: String).size
^
So the compiler knows something about the types of the members.
The inferred T
when you call f(t)
will be some kind of existential type, so not exactly Any
, but in this case morally equivalent to it. As the Foo
case above shows, though, if Event
had other members or methods involving T
the compiler would know that it's the same T
.
When we say the JVM erases types, we really just mean "in generic contexts". Every object (in the JVM sense) has a class associated with it:
scala> val x: Any = "foo"
x: Any = foo
scala> x.getClass
res0: Class[_] = class java.lang.String
But…
scala> val y: Any = Seq(1, 2, 3)
y: Any = List(1, 2, 3)
scala> y.getClass
res1: Class[_] = class scala.collection.immutable.$colon$colon
There are two things to note here. First, the class value we get is a couple of subtype relationships more specific than even the inferred type would have been if we'd left off the : Any
ascription (I'm hand-waving a bit by comparing classes and types, but you know what I mean). Second, because of type erasure for generics, we don't get any information about the element type from y.getClass
, just the "top-level" class of the value.
In my view this is kind of the worst of all possible worlds as far as type erasure is concerned. Sure you can dispatch on types at runtime in Scala!
def foo(x: Any): Unit = x match {
case s: String => println(s"I got a string: $s")
case d: Double => println("numbers suck!")
case xs: List[Int] => println(f"first int is ${ xs.head }%d")
case _ => println("something else")
}
And then:
scala> foo("bar")
I got a string: bar
scala> foo(List(1, 2, 3))
first int is 1
But then:
scala> foo(List(true, false))
java.lang.ClassCastException: java.lang.Boolean cannot be cast to java.lang.Integer
at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:101)
at .foo(<console>:15)
... 31 elided
I'd personally prefer complete erasure of types at runtime (at least as far as the programmer can see) and no type case matching at all. Alternatively we could have .NET-style reified generics (in which case I probably wouldn't be using Scala, but still, it's a reasonable and consistent option). As it is we've got partial type erasure and broken type case matching.
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