Search code examples
scalareflectiondynamic-cast

What is the proper way to wrap isInstanceOf[] calls?


I'd like to build a wrapper for isInstanceOf[T] and asInstanceOf[T] pair that would output Option[T] with convenient map and getOrElse methods.

So I give a try, but the result has disappointed me.

import scala.reflect.runtime.universe.{TypeTag, typeOf}

class Base()
class Deep() extends Base
class Deeper() extends Deep()

final case class WrapSimple[T](source : T) {
  def cast[U] : Option[U] =
    if (source.isInstanceOf[U]) Some(source.asInstanceOf[U]) else None
}

final case class WrapFullTagged[T: TypeTag](source : T) {
  def cast[U : TypeTag] : Option[U] =
    if (typeOf[T] <:< typeOf[U]) Some(source.asInstanceOf[U]) else None
}

final case class WrapHalfTagged[T](source : T) {
  val stpe = {
    val clazz = source.getClass
    val mirror = scala.reflect.runtime.universe.runtimeMirror(clazz.getClassLoader)
    mirror.classSymbol(clazz).toType
  }
  def cast[U : TypeTag] : Option[U] =
    if (stpe <:< typeOf[U]) Some(source.asInstanceOf[U]) else None
}

object Test {
  val base = new Base
  val deep = new Deep
  val deeper = new Deeper
  val wile : Deep = new Deeper

  def testSimple() : Unit = {
    println(WrapSimple(deep).cast[Base].isDefined) // should be true
    println(WrapSimple(deep).cast[Deeper].isDefined) // should be false
    println(WrapSimple(wile).cast[Deeper].isDefined) // should be true
  }

  def testFullTagged() : Unit = {
    println(WrapFullTagged(deep).cast[Base].isDefined) // should be true
    println(WrapFullTagged(deep).cast[Deeper].isDefined) // should be false
    println(WrapFullTagged(wile).cast[Deeper].isDefined) // should be true
  }

  def testHalfTagged() : Unit = {
    println(WrapHalfTagged(deep).cast[Base].isDefined) // should be true
    println(WrapHalfTagged(deep).cast[Deeper].isDefined) // should be false
    println(WrapHalfTagged(wile).cast[Deeper].isDefined) // should be true
  }

  def testAll() : Unit = {
    testSimple()
    testFullTagged()
    testHalfTagged()
  }
}

WrapSimple looks good, but just does not work, it erases the U type in the isInstanceOf[U] method application, so it is always responds with true. The funny thing is that asInstanceOf[U] keeps the U type normally, so it just produces runtime exceptions.

The second approach I had tried is WrapFullTagged that employs type tags. It seems clear enough, but again plainly breaks the contract. It could only check static types at compile time and has zero insight about actual types in runtime.

So, I breed both approaches and gave birth to the third, that at least produces correct output. But it looks awful and invokes power of reflection that comes with a great cost.

Is it possible to solve the issue with greater elegance?


Solution

  • Check out scala.reflect.ClassTag. It provides access to the erased class Type, and according to the api docs for the function with type

    def unapply(x: Any): Option[T]
    

    A ClassTag[T] can serve as an extractor that matches only objects of type T.

    An example that matches the expected output in the question and appears reasonably elegant:

    class Base()
    class Deep() extends Base
    class Deeper() extends Deep()
    
    case object SimpleCaster {
      def cast[A](t: Any)(implicit classTag: scala.reflect.ClassTag[A]): Option[A] = classTag.unapply(t)
    }
    
    object Test {
      val base = new Base
      val deep = new Deep
      val deeper = new Deeper
      val wile: Deep = new Deeper
    
      def testSimple(): Unit = {
        val a = SimpleCaster.cast[Base](deep)
        val b = SimpleCaster.cast[Deeper](deep)
        val c = SimpleCaster.cast[Deeper](wile)
        println(s"${a.isDefined} - ${a.map(_.getClass)}")
        println(s"${b.isDefined} - ${b.map(_.getClass)}")
        println(s"${c.isDefined} - ${c.map(_.getClass)}")
      }
    }
    

    results in the console output:

    scala> Test.testSimple
    true - Some(class Deep)
    false - None
    true - Some(class Deeper)
    

    In summary; whilst this uses the reflect apis, it looks a practical solution that isn't too verbose.