Reputation: 15435
Say, I have a class like this:
class Funky[A, B](val foo: A, val bar: B) {
override def toString: String = s"Funky($foo, $bar)"
}
With some method that looks like this:
def cast(t: Any): Option[Funky[A, B]] = {
if (t == null) None
else if (t.isInstanceOf[Funky[_, _]]) {
val o = t.asInstanceOf[Funky[_, _]]
for {
_ <- typA.cast(o.foo)
_ <- typB.cast(o.bar)
} yield o.asInstanceOf[Funky[A, B]]
} else None
}
How does the isInstanceOf and asInstanceOf work? The Runtime has no information on the actual types that are contained in Funky. So how is this code working? Any clues?
Upvotes: 1
Views: 1361
Reputation: 44967
Let's just take it apart.
Suppose that you somehow obtain instances typA: Typable[A]
and typB: Typable[B]
, so that typA
has a method
def cast(a: Any): Option[A] = ...
and similarly for typB
. The cast
method is expected to return Some[A]
if the argument is indeed of type A
, and None
otherwise. These instances can obviously be constructed for primitive types Int
and String
(they are already provided by the library).
Now you want to use typA
and typB
to implement cast
for Funky[A, B]
.
The null
check should be clear, you can perform it on anything. But then comes the first isInstanceOf
:
else if (t.isInstanceOf[Funky[_, _]]) {
Note that the type arguments of Funky
have been replaced by underscores. This is because the generic parameters of Funky
are erased, and not available at runtime. However, we still can differentiate between Funky[_, _]
and, for example, Map[_, _]
, because the parameterized type itself is preserved, even though the parameters are erased. Moreover, isInstanceOf
could even differentiate between a TreeMap
and a HashMap
, even if both instances had compile-time type Map
: the runtime type is available, it's only the generic parameters that are forgotten.
Similarly, once you know that t
is of type Funky
, you can cast it into
Funky[_, _]
,
val o = t.asInstanceOf[Funky[_, _]]
replacing the generic parameters by existential types. This means: you know only that there are some types X
and Y
such that o
is of type Funky[X, Y]
, but you don't know what those X
and Y
are. However, now you at least know that o
has methods foo
and bar
(even though you don't know what their return types are).
But now you can take o.foo
and o.bar
and feed them into the typA.cast
and typeB.cast
. The underscores on the left hand side of the monadic bind mean that you discard the instances of type A
and B
, which are returned wrapped in Some
. You only care that both casts don't return a None
:
for {
_ <- typA.cast(o.foo)
_ <- typB.cast(o.bar)
} yield /* ... */
If one of the casts failed and returned a None
, the entire monadic expression would evaluate to None
, and so the method would return None
, signifying that the overall cast into Funky[A, B]
has failed.
If both casts succeed, then we know that o
is indeed of type Funky[A, B]
, so we can cast o
into Funky[A, B]
:
o.asInstanceOf[Funky[A, B]]
You might wonder "how is this possible, we don't know anything about A
and B
at runtime!", but this is ok, because this asInstanceOf
is there only to satisfy the type-checking stage of the compiler. It cannot do anything at runtime, because it can only check the Funky
part, but not the erased parameters A
and B
.
Here is a shorter illustration of the phenomenon:
val m: Map[Long, Double] = Map(2L -> 100d)
val what = m.asInstanceOf[Map[Int, String]]
println("It compiles, and the program does not throw any exceptions!")
Just save it as a script and feed it to the scala
interpreter. It will compile and run without complaints, because the asInstanceOf
is blind for anything beyond Map
. So, the second asInstanceOf
is there only to persuade the type-checker that the returned value is indeed of the type Funky[A, B]
, and it is the responsibility of the programmer not to make any nonsensical claims.
To summarize: isInstanceOf
is the thing that does something at runtime (it checks that instances conform to some concrete type, and returns runtime-values true
-false
). The asInstanceOf
has two different functions. The first one (which casts to a concrete class Funky[_, _]
) has a side effect at runtime (the cast can fail and throw an exception). The second one (asInstanceOf[Funky[A, B]]
) is there only to satisfy the type-checking phase at compile time.
Hope this helps somewhat.
Upvotes: 4
Reputation: 18434
You can check that the elements are specific types, with something like:
Funky(1, 2).foo.isInstanceOf[String] // false
Funky(1, 2).foo.isInstanceOf[Int] // true
But if you try to check for a generic type it will not work. For example:
def check[A](x: Any) = x.isInstanceOf[A]
check[String](1) // true
check[String](Funky(1, 2).foo) // true
And the compiler gives you a warning message that explains the error:
abstract type A is unchecked since it is eliminated by erasure
However, the code you showed seems to be working around this by some other method here:
_ <- typA.cast(o.foo)
_ <- typB.cast(o.bar)
Without seeing the implementation of those objects, my guess is they have some sort of TypeTag
or ClassTag
and use that. That is almost always the recommended way to work around erasure.
Upvotes: 1