Dominique Unruh Dominique Unruh - 3 months ago 20
Scala Question

Scala: Typecast without explicitly known type parameter

Consider the following example:

case class C[T](x:T) {
def f(t:T) = println(t)
type ValueType = T
}

val list = List(1 -> C(2), "hello" -> C("goodbye"))

for ((a,b) <- list) {
b.f(a)
}


In this example, I know (runtime guarantee) that the type of
a
will be some
T
, and
b
will have type
C[T]
with the same
T
. Of course, the compiler cannot know that, hence we get a typing error in
b.f(a)
.

To tell the compiler that this invocation is OK, we need to do a typecast à la
b.f(a.asInstanceOf[T])
. Unfortunately,
T
is not known here. So my question is: How do I rewrite
b.f(a)
in order to make this code compile?

I am looking for a solution that does not involve complex constructions (to keep the code readable), and that is "clean" in the sense that we should not rely on code erasure to make it work (see the first approach below).

I have some working approaches, but I find them unsatisfactory for various reasons.

Approaches I tried:



b.asInstanceOf[C[Any]].f(a)


This works, and is reasonably readable, but it is based on a "lie".
b
is not of type
C[Any]
, and the only reason we do not get a runtime error is because we rely on the limitations of the JVM (type erasure). I think it is good style only to use
x.asInstanceOf[X]
when we know that
x
is really of type
X
.

b.f(a.asInstanceOf[b.ValueType])


This should work according to my understanding of the type system. I have added the member
ValueType
to the class
C
in order to be able to explicitly refer to the type parameter
T
. However, in this approach we get a mysterious error message:

Error:(9, 22) type mismatch;
found : b.ValueType
(which expands to) _1
required: _1
b.f(a.asInstanceOf[b.ValueType])
^


Why? It seems to complain that we expect type
_1
but got type
_1
! (But even if this approach works, it is limited to the cases where we have the possibility to add a member
ValueType
to
C
. If
C
is some existing library class, we cannot do that either.)

for ((a,b) <- list.asInstanceOf[List[(T,C[T]) forSome {type T}]]) {
b.f(a)
}


This one works, and is semantically correct (i.e., we do not "lie" when invoking
asInstanceOf
). The limitation is that this is somewhat unreadable. Also, it is somewhat specific to the present situation: if
a,b
do not come from the same iterator, then where can we apply this type cast? (This code also has the side effect of being too complex for Intelli/J IDEA 2016.2 which highlights it as an error in the editor.)

val (a2,b2) = (a,b).asInstanceOf[(T,C[T]) forSome {type T}]
b2.f(a2)


I would have expected this one to work since
a2,b2
now should have types
T
and
C[T]
for the same existential
T
. But we get a compile error:

Error:(10, 9) type mismatch;
found : a2.type (with underlying type Any)
required: T
b2.f(a2)
^


Why? (Besides that, the approach has the disadvantage of incurring runtime costs (I think) because of the creation and destruction of a pair.)

b match {
case b : C[t] => b.f(a.asInstanceOf[t])
}


This works. But enclosing the code with a match makes the code much less readable. (And it also is too complicated for Intelli/J.)

Answer

The cleanest solution is, IMO, the one you found with the type-capture pattern match. You can make it concise, and hopefully readable, by integrating the pattern directly inside your for comprehension, as follows:

for ((a, b: C[t]) <- list) {
  b.f(a.asInstanceOf[t])
}

Fiddle: http://www.scala-js-fiddle.com/gist/b9030033133ee94e8c18ad772f3461a0

If you are not in a for comprehension already, unfortunately the corresponding pattern assignment does not work:

val (c, d: C[t]) = (a, b)
d.f(c.asInstanceOf[t])

That's because t is not in scope anymore on the second line. In that case, you would have to use the full pattern matching.