34

I would like to write a function that casts to type A, where A can be e.g. List[Int], or a more complicated parameterized type like Map[Int, List[Int]].

def castToType[A](x: Any): A = {
  // throws if A is not the right type
  x.asInstanceOf[A]
}

Right now, due to type erasure (I believe), the code merrily works even when the type is not correct. The error only manifests on access, witha ClassCastException.

val x = List(1, 2, 3)
val y = castToType[List[String]](x)
y(0) --> throws java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

Is there a way I can use manifests to make this work properly? Thanks!

Pandora Lee
  • 1,435
  • 2
  • 14
  • 17
  • Can you provide more context on why you need to cast? In your example `val x = List(1, 2, 3)` the compiler infers `List[Int]` so it seems pointless to use castToType. If you can avoid `Any` and `asInstanceOf` altogether, that's the best option. – huynhjl Jul 14 '11 at 01:59
  • Yeah, basically my DB/memcached interface returns an Any, and I have a typed class (see http://stackoverflow.com/questions/5985076/scala-generic-class-supporting-function-of-multiple-arities) that needs to return a type R. So I need to do some kind of cast to make the compiler happy, but I'd like it to also just throw if the cast is incorrect. – Pandora Lee Jul 14 '11 at 03:39

5 Answers5

20

Unfortunately, this in an inherent limitation of asInstanceOf. I'm actually surprised to see the scaladoc mention it in details:

Note that the success of a cast at runtime is modulo Scala's erasure semantics. Therefore the expression 1.asInstanceOf[String] will throw a ClassCastException at runtime, while the expression List(1).asInstanceOf[List[String]] will not. In the latter example, because the type argument is erased as part of compilation it is not possible to check whether the contents of the list are of the requested type.

If you're mainly concerned about failing fast on wrong cast for traversable which would likely be the main issue when getting stuff back from your DB/memcached interface, I was playing around forcing a cast of the head for traversable objects:

def failFastCast[A: Manifest, T[A] <: Traversable[A]](as: T[A], any: Any) = { 
  val res = any.asInstanceOf[T[A]]
  if (res.isEmpty) res 
  else { 
    manifest[A].newArray(1).update(0, res.head) // force exception on wrong type
    res
  }
}

On a simple example it works:

scala> val x = List(1, 2, 3): Any
x: Any = List(1, 2, 3)

scala> failFastCast(List[String](), x)
java.lang.ArrayStoreException: java.lang.Integer
[...]

scala> failFastCast(List[Int](), x)
res22: List[Int] = List(1, 2, 3)

But not on a more complex one:

val x = Map(1 -> ("s" -> 1L)): Any
failFastCast(Map[Int, (String, String)](), x) // no throw

I wonder if there is a way to recursively drill down into A to keep casting until there is no more type parameters...

huynhjl
  • 41,520
  • 14
  • 105
  • 158
12

You are indeed correct - type erasure means that you cannot "cast" in such a way as to distinguish between List[Int] and List[String], for example. However, you can improve on the cast which you are performing, whereby A is erased in such a way as to mean that you cannot distinguish between an Int and a String:

def cast[A](a : Any) = a.asInstanceOf[A]
//... is erased to
def erasedCast(a : Any) = a.asInstanceOf[Any]

What you need are reified generics, using manifests

def cast[A <: AnyRef : Manifest](a : Any) : A 
  = manifest[A].erasure.cast(a).asInstanceOf[A]

Whilst the final cast is erased to AnyRef, at least you should have the correct Class[_] instance (manifest.erasure) to get the top level type correct. In action:

scala> cast[String]("Hey")
res0: String = Hey

scala> cast[java.lang.Integer]("Hey")
  java.lang.ClassCastException
    at java.lang.Class.cast(Class.java:2990)
    at .cast(<console>:7)
    at .<init>(<console>:9)

scala> cast[List[String]](List("Hey"))
res2: List[String] = List(Hey)

scala> cast[List[Int]](List("Hey"))
res3: List[Int] = List(Hey)

My advice is not to use nested reflection to decide whether the target was really a List[Int]: this is not generally feasible. For what should the following return?

cast[List[Int]](List[AnyVal](1, 2))
oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449
5

You could use shapeless's Typeable from Miles Sabin:

Type casting using type parameter

It handles erasure in many cases, though only specific ones:

scala> import shapeless._; import syntax.typeable._
import shapeless._
import syntax.typeable._

scala> val x = List(1, 2, 3)
x: List[Int] = List(1, 2, 3)

scala> val y = x.cast[List[String]]
y: Option[List[String]] = None

To see the set of cases that it handles you can refer to its source:

https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/typeable.scala

Community
  • 1
  • 1
tksfz
  • 2,932
  • 1
  • 23
  • 25
2

Consider this solution:

trait -->[A, B] {
  def ->(a: A): B
}

implicit val StringToInt = new -->[String, Int] {
  def ->(a: String): Int = a.toInt
}

implicit val DateToLong = new -->[java.util.Date, Long] {
  def ->(a: java.util.Date): Long = a.getTime
}

def cast[A,B](t:A)(implicit ev: A --> B):B= ev.->(t)

The advantage is that:

  1. It is type safe - the compiler will tell you if the type cannot be casted
  2. You can define casting rules by providing proper implicits

Now you can use it so:

scala>  cast(new java.util.Date())
res9: Long = 1361195427192

scala>  cast("123")
res10: Int = 123

EDIT

I've spent some time and wrote this advanced code. First let me show how to use it:

scala>    "2012-01-24".as[java.util.Date]
res8: java.util.Date = Tue Jan 24 00:00:00 CET 2012

scala>    "2012".as[Int]
res9: Int = 2012

scala>    "2012.123".as[Double]
res12: Double = 2012.123

scala>    "2012".as[Object]   // this is not working, becouse I did not provide caster to Object
<console>:17: error: could not find implicit value for parameter $greater: -->[String,Object]
"2012".as[Object]
^

Pretty nice? See the scala magic:

trait -->[A, B] {
  def ->(a: A): B
}

implicit val StringToInt = new -->[String, Int] {
  def ->(a: String): Int = a.toInt
}

implicit val StringToDate = new -->[String, java.util.Date] {
  def ->(a: String): java.util.Date = (new java.text.SimpleDateFormat("yyyy-MM-dd")).parse(a)
}

implicit val StringToDouble = new -->[String, Double] {
  def ->(a: String): Double = a.toDouble
}

trait AsOps[A] {
  def as[B](implicit > : A --> B): B
}

implicit def asOps[A](a: A) = new AsOps[A] {
  def as[B](implicit > : A --> B) = > ->(a)
}
kiritsuku
  • 52,967
  • 18
  • 114
  • 136
pawel.panasewicz
  • 1,831
  • 16
  • 27
  • 2
    If you allow a bit of nitpicking, you are demonstrating a *conversion*, as opposed to a cast. It is true that the ability to express conversions right through the type system is one of the most beautiful features of Scala (and Haskell), but *exactly the problem of type erasure* is what hampers use of this feature in many cases in practice: If you've lost the detailed type information an are left just with an `AnyVal`, you've lost also the ability to pick an conversion based on the detailed type – Ichthyo Jan 18 '14 at 12:24
2

Yes, the problem occurs due to type erasure. If you try

val x = List(1,2,3)
val y = castToType[Int](x)

The exception is thrown right away, as expected. The same occurs when trying to cast to Array[String] or even Array[Int].

I don't think you can create a generic type converter that works will types inside collections and other objects. You will need to create a converter for each object type. For example:

def castToType[A](x: List[A]) = x.map(i => i.asInstanceOf[A])
rafalotufo
  • 3,862
  • 4
  • 25
  • 28
  • Isn't your `castToType` method just a fancy identity function? Did you mean for the parameter to be of type `List[Any]`? Interestingly, even if you make it a `List[Any]` it still doesn't throw. – Aaron Novstrup Jul 14 '11 at 05:59