简体   繁体   中英

No TypeTag available for case class Type

I want to generate a method which will convert an Object into a Map[String, _] , and later back from Map[String, _] to Object .

I generate the initial object as follows:

  case class Name (firstName : String, lastName : String)
  case class Documents (idx: String, name: Name, code: String)

  val mName1 = Name("Roger", "Rabbit")
  val myDoc = Documents("12", mName1, "ABCD")

Then following method converts a given Map[String, _] into an Object :

def fromMap[T : TypeTag: ClassTag ](m: Map[String,_]) = {
    val rm = runtimeMirror(classTag[T].runtimeClass.getClassLoader)
    val classTest = typeOf[T].typeSymbol.asClass
    val classMirror = rm.reflectClass(classTest)
    val constructor = typeOf[T].decl(termNames.CONSTRUCTOR).asMethod
    val constructorMirror = classMirror.reflectConstructor(constructor)

    val constructorArgs = constructor.paramLists.flatten.map( (param: Symbol) => {
      val paramName = param.name.toString
      if(param.typeSignature <:< typeOf[Option[Any]])
        m.get(paramName)
      else
        m.get(paramName).getOrElse(throw new IllegalArgumentException("Map is missing required parameter named " + paramName))
    })

    constructorMirror(constructorArgs:_*).asInstanceOf[T]
  }

And inside the following method I convert the initial Object into a Map[String, _] , and back to Object (by invoking the method above):

def fromMapToObject(input: Any) : Unit= {

    println("input: "+input)

    //Converting an Object into a Map
    val r = currentMirror.reflect(input)
    val docAsMapValues = r.symbol.typeSignature.members.toStream
      .collect{case s : TermSymbol if !s.isMethod => r.reflectField(s)}
      .map(r => r.symbol.name.toString.trim -> r.get)
      .toMap

    println("intermediate: "+docAsMapValues)


    val obj = fromMap[Documents](docAsMapValues)
    println("output: "+obj)

  }

So if I call:

 fromMapToObject(myDoc)

Input and output will match.

Problem , trying to go a step further, I want now to do the same with the field name , which is of type Name . But I want this step to be generic, in the sense that without knowing what is the type of the field name , I could convert it into a Map[String, _] , and from Map[String, _] back to Object .

So what I will do now in fromMapToObject is:

  1. Extract from the input a Map[String, _]
  2. Extract from the input a Map[String, Types]
  3. Convert the value of the field name from Name into a Map[String, _]
  4. Revert the 3rd step to get back an Object of type Name

This is how I am trying to approach this new scenario:

def fromMapToObject[T: TypeTag: ClassTag](input: Any) : Unit = {

    println("input: "+input)

    //Converting an Object into a Map
    val r = currentMirror.reflect(input)
    val docAsMapValues = r.symbol.typeSignature.members.toStream
      .collect{case s : TermSymbol if !s.isMethod => r.reflectField(s)}
      .map(r => r.symbol.name.toString.trim -> r.get)
      .toMap

    val docAsMapTypes = r.symbol.typeSignature.members.toStream
      .collect{case s : TermSymbol if !s.isMethod => r.reflectField(s)}
      .map(r => r.symbol.name.toString.trim -> r.symbol.typeSignature)
      .toMap

    // Here I extract from the map the value and type of the attribute name 
    val nameType = docAsMapValues("name")
    val nameValue =  docAsMapValues("name")

    // Converting Name into a map
    val r2 = currentMirror.reflect(nameValue)
    val nameAsMapValues = r2.symbol.typeSignature.members.toStream
      .collect{case s : TermSymbol if !s.isMethod => r2.reflectField(s)}
      .map(r2 => r2.symbol.name.toString.trim -> r2.get)
      .toMap

    type nameT = nameType.type
    val obj = fromMap[nameT](nameAsMapValues)

}

But I am getting the following error when compiling in Intellij:

Error:(111, 29) No TypeTag available for nameT
    val obj = fromMap[nameT](nameAsMapValues)

I would like to know how could I convert that runtime.universe.Type which is returned from r.symbol.typeSignature into a TypeTag : ClassTag

I'm not completely certain that I'm interpreting your question correctly, but from what I understand this can be solved pretty nicely and type safely via shapeless. To start with, you want to convert your Document to a Map . shapeless can do this out of the box for you with one of the Typeclasses in the ops folder. If we bundle that up into a function, with some machinery to pull everything together, we get something like:

import shapeless._

def ObjectToMap[A, Repr <: HList](obj: A)(
  implicit
  gen: LabelledGeneric.Aux[A,Repr], //Convert to generic representation
  toMap: ops.record.ToMap[Repr] //Convert generic representation to Map[Symbol,_]
) = toMap(gen.to(obj))

which outputs

val m = ObjectToMap(myDoc)
println(m) //Map('code -> ABCD, 'name -> Name(Roger,Rabbit), 'idx -> 12)

Going the other direction is a little bit more complicated. There exists a ops.maps.FromMap typeclass. However, we want to be able to specify the type parameter, and then let the compiler still verify that the generic representation is an HList , to match up with FromMap 's signature. Since dependent types don't work with other variables defined in the same parameter list, and we only get one implicit parameter list, we need to resort to a little bit of trickery to curry the type parameters:

trait MapToObject[A]{
  def from[Repr <: HList](m: Map[_,_])(
    implicit
    gen: LabelledGeneric.Aux[A,Repr],
    fromMap: ops.maps.FromMap[Repr]
  ): Option[A] = fromMap(m).map(gen.from)
}

object MapToObject{
  def apply[A](
    implicit gen: LabelledGeneric[A]
  ): MapToObject[A] = new MapToObject[A]{}
}

When we run the output of the previous chunk through that we get:

val doc = MapToObject[Documents].from(m)
println(doc) //Some(Documents(12,Name(Roger,Rabbit),ABCD))

type nameT = nameType.type is incorrect. nameType.type is (singleton) type of this specific variable nameType and you want type of name field. This accidentally worked because actually you don't use T in fromMap2 ( runtimeMirror(classTag[T].runtimeClass.getClassLoader) can be replaced with currentMirror there).

You wanted to call your original fromMap inside fromMapToObject . You know universe.Type of name and it's enough to find TypeTag and ClassTag implicit parameters for fromMap . But it's not enough to find T . The thing is that since you use runtime reflection you know universe.Type (and TypeTag , ClassTag ) at runtime. But in order to call fromMap you need to know T at compile time. So one way is to use compile-time reflection ie macros. Other way is to avoid T and use value parameters like you did.

Case class to map in Scala

Scala: convert map to case class

I was able to find a solution. Since I wasn't able to get classTag and typeTag I modified the function toMap as follow:

def fromMap2[T : ClassTag ](m: Map[String,_], mSymbol: Symbol, mType :Type): Any = {
    val rm = runtimeMirror(classTag[T].runtimeClass.getClassLoader)
    val classTest = mSymbol.asClass
    val classMirror = rm.reflectClass(classTest)
    val constructor = mType.decl(termNames.CONSTRUCTOR).asMethod
    val constructorMirror = classMirror.reflectConstructor(constructor)

    val constructorArgs = constructor.paramLists.flatten.map( (param: Symbol) => {
      val paramName = param.name.toString
      if(param.typeSignature <:< typeOf[Option[Any]])
        m.get(paramName)
      else
        m.get(paramName).getOrElse(throw new IllegalArgumentException("Map is missing required parameter named " + paramName))
    })

    constructorMirror(constructorArgs:_*).asInstanceOf[T]

  }

So now I need to pass the Type and Symbol of T. I can get these two values like follow:

//Converting an Object into a Map
val r = currentMirror.reflect(input)
val mapValues = r.symbol.typeSignature.members.toStream
  .collect{case s : TermSymbol if !s.isMethod => r.reflectField(s)}
  .map(r => r.symbol.name.toString.trim -> r.get)
  .toMap

val mapTypes = r.symbol.typeSignature.members.toStream
  .collect{case s : TermSymbol if !s.isMethod => r.reflectField(s)}
  .map(r => r.symbol.name.toString.trim -> r.symbol.typeSignature)
  .toMap

val mapTypesSymbols = r.symbol.typeSignature.members.toStream
  .collect{case s : TermSymbol if !s.isMethod => r.reflectField(s)}
  .map(r => r.symbol.name.toString.trim -> r.symbol.typeSignature.typeSymbol)
  .toMap

val nameType = mapTypes("name")
val nameTypeSymbols =  mapTypesSymbols("name")
val nameValue =  mapValues("name")

// Converting Name into a map
    val r2 = currentMirror.reflect(nameValue)
    val nameAsMapValues = r2.symbol.typeSignature.members.toStream
      .collect{case s : TermSymbol if !s.isMethod => r2.reflectField(s)}
      .map(r2 => r2.symbol.name.toString.trim -> r2.get)
      .toMap

type nameT = nameType.type

val obj = fromMap2[nameT](nameAsMapValues, nameTypeSymbols, nameType)

Even if this works, I believe it is very anti pattern. So I will leave the question open in case someone could point out ways to improve it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM