ayvango ayvango - 2 months ago 5
Scala Question

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?

Answer

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.