18

I am trying to achieve to write a method that casts a value of Any to a specific type and returns option instead of throwing an exception like instanceOf. Scala does not behave like i have expected it:

def cast[A](value: Any): Option[A] =
{
  try
  {
    Some(value.asInstanceOf[A])
  } catch
  {
    case e: Exception => None
  }
}

The test:

val stringOption: Option[String] = cast[String](2)
stringOption must beNone

fails with the Error

java.lang.Exception: 'Some(2)' is not None

Somebody has an idea why?

3 Answers3

21

Erasure rains on your parade here. Therefore at runtime the type A is not known anymore and asInstanceOf[A] is compiled to a no-op. It just makes the compiler believe that the resulting value is of type A, but that is not actually ensured at runtime.

You can use Scala's manifests to work around it, though. Unfortunately the JVM's handling of primitive types / boxing forces us to do some extra work.

The following works, although it doesn't handle "weak conformance" of types, meaning that e.g. an Int is not considered a Long, so cast[Long](42) returns None.

def cast[A : Manifest](value: Any): Option[A] = {
  val erasure = manifest[A] match {
    case Manifest.Byte => classOf[java.lang.Byte]
    case Manifest.Short => classOf[java.lang.Short]
    case Manifest.Char => classOf[java.lang.Character]
    case Manifest.Long => classOf[java.lang.Long]
    case Manifest.Float => classOf[java.lang.Float]
    case Manifest.Double => classOf[java.lang.Double]
    case Manifest.Boolean => classOf[java.lang.Boolean]
    case Manifest.Int => classOf[java.lang.Integer]
    case m => m.erasure
  }
  if(erasure.isInstance(value)) Some(value.asInstanceOf[A]) else None
}
Ruediger Keller
  • 3,024
  • 2
  • 20
  • 17
  • it's not true - asInstanceOf always compiles into checkcast operation for non-primitive types and generics – dk14 Dec 01 '14 at 04:20
5

This is because of type erasure. At runtime, A in Option[A] is not known, so you are permitted to store a Some(3) in a variable of type Option[String].

The exception will occur when the value inside the option is accessed:

scala> val result = cast[String](2)
result: Option[String] = Some(2)

scala> result.get
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
        at .<init>(<console>:10)
        at .<clinit>(<console>)
        // ...
Ben James
  • 121,135
  • 26
  • 193
  • 155
  • Why does `getOrElse(42)` not generate this exception but evaluate to 2? –  Aug 14 '11 at 20:34
  • 2
    The return type of `Option[A].getOrElse[B]` must be a supertype of both `A` and `B`. In the case of `String` and `Int`, the return type is `Any`, and you can of course cast `2` to `Any`. If you tried `getOrElse("42")` you'd get a `ClassCastException`, since the return type would be `String`. – Ben James Aug 14 '11 at 20:40
  • Makes sense, so there is no easy way to write a method like that in a generic way? – Bastian Echterhölter Aug 14 '11 at 20:42
  • I was going to reply "you might be able to do this with Manifests" - well, see Ruediger's answer :) – Ben James Aug 14 '11 at 21:38
2

I did pretty much the same thing just now, with Scala 2.10, TypeTags(due to type erasure) and ValidationNEL from scalaz:

import scala.reflect.runtime.universe._

def as[T: TypeTag](term: Any): ValidationNEL[String, T] =
  if (reflect.runtime.currentMirror.reflect(term).symbol.toType <:< typeOf[T])
    term.asInstanceOf[T].successNel[String]
  else
    ("Cast error: " + term + " to " + typeOf[T]).failNel[T]

With Option instead of Validation it would look like this:

  def as[T: TypeTag](term: Any): Option[T] =
  if (reflect.runtime.currentMirror.reflect(term).symbol.toType <:< typeOf[T])
    Some(term.asInstanceOf[T])
  else
    None

I got my infos here: How to know if an object is an instance of a TypeTag's type? , Runtime resolution of type arguments using scala 2.10 reflection

Community
  • 1
  • 1
schlicht
  • 4,735
  • 1
  • 13
  • 23