scalascala-reflect

Trying to extract the TypeTag of a Sequence of classes that extend a trait with different generic type parameters


The following code example shows the core of my question:

// This is the base trait that the classes are extending
trait Operation[T] {
  def operate(x: T): T
}

// Here are 2 case classes that exist for the sole purpose of being
// the generic type for the classes I'm making
case class CaseClass1(field1_1: String, field1_2: String, field1_3: String)
case class CaseClass2(field2_1: String, field2_2: String, field2_3: String)

// These are the 2 classes that extend my basic trait, implementing the operate
// method with some kind of logic
class FillField1 extends Operation[CaseClass1] {
  def operate(input: CaseClass1): CaseClass1 = input.copy(field1_3 = "haha")
}
class FillField2 extends Operation[CaseClass2] {
  def operate(input: CaseClass2): CaseClass2 = input.copy(field2_2 = "hoho")
}

import scala.reflect.runtime.universe._
// This is a function that prints out the typetag information currently available
def someFunc[T: TypeTag](x: Operation[T]): Unit = {
  println(s"TypeTag is: ${typeOf[T]}")
}

// Here I'm creating a sequence of Operations, but they can have any generic
// type parameter
val someSeq: Seq[Operation[_]] = Seq(new FillField1, new FillField2)

someSeq.map(someFunc(_))
/*
  Output:
  TypeTag is: _$1
  TypeTag is: _$1
*/

someFunc(new FillField1)
/*
  Output:
  TypeTag is: CaseClass1
*/

As you can see, when I call someFunc(new fillField1) I can properly find my typetag at runtime. But when I'm using someSeq, which is a Sequence that can contain multiple types of classes I can't get the typetag I need at runtime. Is this because you lose that information at runtime?

How can I get the proper typetag at runtime? So how could I get as output TypeTag is: CustomClass1 and TypeTag is: CustomClass2 when I'm using that Seq[Operation[_]]?

I'm working on an Apache Spark project where we have a structure similar to this and when I'm using that sequence I'm getting an issue that the TypeTag points to an unknown class, _$10 (or whatever name the compiler made for my typetag), instead of the actual TypeTag which would be CustomClass1 or CustomClass2...


Solution

  • What TypeTag mostly does is not runtime reflection but persisting some information (a type) from compile time to runtime.

    Seq is a homogeneous collection (i.e. all its elements have the same type). In Seq(new FillField1, new FillField2) both elements have type Operation[_]. So when someFunc is applied T is inferred to be _ aka _$1 (i.e. unknown argument of existential type Operation[_]).

    So one option is to use a heterogeneous collection (HList). Then the elements can have different types, these types can be captured from compile time to runtime, the types can be handled at runtime

    import shapeless.{HNil, Poly1}
    
    object someFuncPoly extends Poly1 {
      implicit def cse[T: TypeTag, O](implicit ev: O <:< Operation[T]): Case.Aux[O, Unit] =
        at(x => someFunc(x))
    }
    
    def someFunc[T: TypeTag](x: Operation[T]): Unit = println(s"Type is: ${typeOf[T]}")
    
    (new FillField1 :: new FillField2 :: HNil).map(someFuncPoly)
    //Type is: CaseClass1
    //Type is: CaseClass2
    

    Another option is to use runtime reflection (i.e. what TypeTag doesn't do)

    import scala.reflect.runtime.universe._
    import scala.reflect.runtime
    val runtimeMirror = runtime.currentMirror
    
    def someFunc(x: Operation[_]): Unit = {
      val xSymbol = runtimeMirror.classSymbol(x.getClass)
      val operationSymbol = xSymbol.baseClasses(1)// or just typeOf[Operation[_]].typeSymbol if you know Operation statically
      val extendee = xSymbol.typeSignature/*toType*/.baseType(operationSymbol)
      println(s"Type is: ${extendee.typeArgs.head}")
    }
    
    someSeq.map(someFunc(_))
    //Type is: CaseClass1
    //Type is: CaseClass2
    

    Another implementation is

    def someFunc(x: Operation[_]): Unit = {
      val xSymbol = runtimeMirror.classSymbol(x.getClass)
      val operationSymbol = xSymbol.baseClasses(1).asClass
      val operationParamType = operationSymbol.typeParams(0).asType.toType
      println(s"Type is: ${operationParamType.asSeenFrom(xSymbol.toType, operationSymbol)}")
    }
    

    One more option is magnet pattern (1 2 3 4 5 6 7)

    trait Magnet[T] {
      def someFunc: Unit
    }
    
    import scala.language.implicitConversions
    
    implicit def operationToMagnet[T: TypeTag](x: Operation[T]): Magnet[T] = new Magnet[T] {
      override def someFunc: Unit = println(s"TypeTag is: ${typeOf[T]}")
    }
    
    def someFunc[T: TypeTag](x: Operation[T]): Unit = (x: Magnet[T]).someFunc
    
    Seq[Magnet[_]](new FillField1, new FillField2).map(_.someFunc)
    // TypeTag is: CaseClass1
    // TypeTag is: CaseClass2
    

    Alternatively you can move someFunc inside Operation, move TypeTag from the method to the class and make Operation an abstract class

    abstract class Operation[T: TypeTag] {
      def operate(x: T): T
    
      def someFunc: Unit = {
        println(s"TypeTag is: ${typeOf[T]}")
      }
    }
    
    (new FillField1).someFunc //TypeTag is: CaseClass1
    (new FillField2).someFunc //TypeTag is: CaseClass2
    
    someSeq.map(_.someFunc)
    //TypeTag is: CaseClass1
    //TypeTag is: CaseClass2