Search code examples
scalatype-erasurescala-reflect

In Scala 2.11+ reflection, how to reliably convert a TypeTag and a Manifest into each other?


In this post:

Is it possible to convert a TypeTag to a Manifest?

It is indicated that a TypeTag can be converted into a Manifest using the following code:

  def toManifest[T:TypeTag]: Manifest[T] = {
    val t = typeTag[T]
    val mirror = t.mirror
    def toManifestRec(t: Type): Manifest[_] = {
      val clazz = ClassTag[T](mirror.runtimeClass(t)).runtimeClass
      if (t.typeArgs.length == 1) {
        val arg = toManifestRec(t.typeArgs.head)
        ManifestFactory.classType(clazz, arg)
      } else if (t.typeArgs.length > 1) {
        val args = t.typeArgs.map(x => toManifestRec(x))
        ManifestFactory.classType(clazz, args.head, args.tail: _*)
      } else {
        ManifestFactory.classType(clazz)
      }
    }
    toManifestRec(t.tpe).asInstanceOf[Manifest[T]]
  }

It doesn't work, as demonstrated in the following test case:

object TypeTag2Manifest {

  class Example {

    type T = Map[String, Int]
  }
  val example = new Example

}

class TypeTag2Manifest extends FunSpec {

  import org.apache.spark.sql.catalyst.ScalaReflection.universe._
  import TypeTag2Manifest._

  it("can convert") {

    val t1 = implicitly[TypeTag[example.T]]
    val v1 = toManifest(t1)
    val v2 = implicitly[Manifest[example.T]]

    assert(v1 == v2)
  }
}

Output:

scala.collection.immutable.Map did not equal scala.collection.immutable.Map[java.lang.String, Int]
ScalaTestFailureLocation: com.tribbloids.spike.scala_spike.reflection.TypeTag2Manifest at (TypeTag2Manifest.scala:52)
Expected :scala.collection.immutable.Map[java.lang.String, Int]
Actual   :scala.collection.immutable.Map

Evidently this indicates that type erasure has been plaguing the conversion, and TypeTag, despite being designed to avoid type erasure, can only resolve to the dependent type example.T without getting the correct type arguments from underlying type Map[String, Int].

So what is the way to convert TypeTag and Manifest into each other that doesn't suck?


Solution

  • If you replace

    toManifestRec(t.tpe).asInstanceOf[Manifest[T]]
    

    with

    toManifestRec(t.tpe.dealias).asInstanceOf[Manifest[T]]
    

    this improves toManifest but not completely:

    scala.collection.immutable.Map[java.lang.String, int]
    

    vs.

    scala.collection.immutable.Map[java.lang.String, Int]
    

    i.e. Java primitive int vs. scala.Int.

    The problem is in the second .runtimeClass in

    val clazz = ClassTag[T](mirror.runtimeClass(t)).runtimeClass 
    

    mirror.runtimeClass(t) is int, ClassTag[T](mirror.runtimeClass(t)) is Int but ClassTag[T](mirror.runtimeClass(t)).runtimeClass is again int.

    So try to improve toManifest more

    def toManifest[T: TypeTag]: Manifest[T] = {
      val t = typeTag[T]
      val mirror = t.mirror
      def toManifestRec(t: Type): Manifest[_] = {
        ClassTag[T](mirror.runtimeClass(t)) match {
          case ClassTag.Byte    => Manifest.Byte
          case ClassTag.Short   => Manifest.Short
          case ClassTag.Char    => Manifest.Char
          case ClassTag.Int     => Manifest.Int
          case ClassTag.Long    => Manifest.Long
          case ClassTag.Float   => Manifest.Float
          case ClassTag.Double  => Manifest.Double
          case ClassTag.Boolean => Manifest.Boolean
          case ClassTag.Unit    => Manifest.Unit
          case ClassTag.Object  => Manifest.Object
          case ClassTag.Nothing => Manifest.Nothing
          case ClassTag.Null    => Manifest.Null
          case classTag         =>
            val clazz = classTag.runtimeClass
            if (t.typeArgs.length >= 1) {
              val args = t.typeArgs.map(x => toManifestRec(x))
              ManifestFactory.classType(clazz, args.head, args.tail: _*)
            } else {
              ManifestFactory.classType(clazz)
            }
        }
      }
      toManifestRec(t.tpe.dealias).asInstanceOf[Manifest[T]]
    }
    
    val t1 = implicitly[TypeTag[example.T]] //TypeTag[TypeTag2Manifest.example.T]
    val v1 = toManifest(t1) //scala.collection.immutable.Map[java.lang.String, Int]
    val v2 = implicitly[Manifest[example.T]] //scala.collection.immutable.Map[java.lang.String, Int]
    v1 == v2 //true