简体   繁体   中英

How to use ClassTag in scala macros implemented for type

I wrote a macros, that reads class fields:

import scala.language.experimental.macros
import scala.reflect.macros.whitebox

object ArrayLikeFields {
  def extract[T]: Set[String] = macro extractImpl[T]

  def extractImpl[T: c.WeakTypeTag](c: whitebox.Context): c.Expr[Set[String]] = {

    import c.universe._

    val tree = weakTypeOf[T].decls
      .collectFirst {
        case m: MethodSymbol if m.isPrimaryConstructor => m
      }
      .map(y => y.paramLists.headOption.getOrElse(Seq.empty))
      .getOrElse(Seq.empty)
      .map(s => q"${s.name.decodedName.toString}")

    c.Expr[Set[String]] {
      q"""Set(..$tree)"""
    }
  }

}

I'm able to compile and run it for concrete type:

object Main extends App {
  case class Person(name:String)
  val res: Set[String] = ArrayLikeFields.extract[Person]
}

But i want use it with generic types like that:

object Lib {
  implicit class SomeImplicit(s: String) {

    def toOrgJson[T]: JSONObject = {
      val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T]
      //some code, that uses fields, etc
      null
    }
  }
}

Compilation error:

Error:(14, 65) type mismatch; found : scala.collection.immutable.Set[Nothing] required: Set[String] Note: Nothing <: String, but trait Set is invariant in type A. You may wish to investigate a wildcard type such as _ <: String . (SLS 3.2.10) val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T]

I can't understund that. How can I solve my problem?

upd
I read scala 2.10.2 calling a 'macro method' with generic type not work about materialisation, but i have no instance of class

Try approach with materializing a type class like in 1

object Main extends App {
  case class Person(name:String)
  val res: Set[String] = ArrayLikeFields.extract[Person] //Set(name)

  import Lib._
  "abc".toOrgJson[Person] // prints Set(name)
}

object Lib {
  implicit class SomeImplicit(s: String) {
    def toOrgJson[T: ArrayLikeFields.Extract]: JSONObject = {
      val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T]
      //some code, that uses fields, etc
      println(arrayLikeFields) //added
      null
    }
  }
}

import scala.language.experimental.macros
import scala.reflect.macros.whitebox

object ArrayLikeFields {    
  def extract[T](implicit extr: Extract[T]): Set[String] = extr()

  trait Extract[T] {
    def apply(): Set[String]
  }

  object Extract {
    implicit def materializeExtract[T]: Extract[T] = macro materializeExtractImpl[T]

    def materializeExtractImpl[T: c.WeakTypeTag](c: whitebox.Context): c.Expr[Extract[T]] = {
      import c.universe._

      val tree = weakTypeOf[T].decls
        .collectFirst {
          case m: MethodSymbol if m.isPrimaryConstructor => m
        }
        .map(y => y.paramLists.headOption.getOrElse(Seq.empty))
        .getOrElse(Seq.empty)
        .map(s => q"${s.name.decodedName.toString}")

      c.Expr[Extract[T]] {
        q"""new ArrayLikeFields.Extract[${weakTypeOf[T]}] {
          override def apply(): _root_.scala.collection.immutable.Set[_root_.java.lang.String] =
            _root_.scala.collection.immutable.Set(..$tree)
        }"""
      }
    }
  }
}

Actually, I don't think you need whitebox macros here, blackbox ones should be enough. So you can replace (c: whitebox.Context) with (c: blackbox.Context) .

By the way, the same problem can be solved with Shapeless rather than macros (macros work in Shapeless under the hood)

object Main extends App {
  case class Person(name:String)
  val res: Set[String] = ArrayLikeFields.extract[Person] //Set(name)
}

object ArrayLikeFields {
  def extract[T: Extract]: Set[String] = implicitly[Extract[T]].apply()

  trait Extract[T] {
    def apply(): Set[String]
  }

  object Extract {
    def instance[T](strs: Set[String]): Extract[T] = () => strs

    implicit def genericExtract[T, Repr <: HList](implicit
      labelledGeneric: LabelledGeneric.Aux[T, Repr],
      extract: Extract[Repr]
      ): Extract[T] = instance(extract())

    implicit def hconsExtract[K <: Symbol, V, T <: HList](implicit
      extract: Extract[T],
      witness: Witness.Aux[K]
      ): Extract[FieldType[K, V] :: T] =
      instance(extract() + witness.value.name)

    implicit val hnilExtract: Extract[HNil] = instance(Set())
  }
}

The answer on the linked question, scala 2.10.2 calling a 'macro method' with generic type not work , also applies here.

You are trying to solve a run-time problem with a compile-time macro, which is not possible.

The called method toOrgJson[T] cannot know the concrete type that T represents at compile time, but only gets that information at run-time. Therefore, you will not be able to do any concrete operations on T (such as listing its fields) at compile-time, only at run-time.

You can implement an operation like ArrayLikeFields.extract[T] at run-time using Reflection, see eg Get field names list from case class

I don't have a very solid understanding of Macros, but it seems that the compiler does not understand that the return type of the macro function is Set[String] .

The following trick worked for me in scala 2.12.7

def toOrgJson[T]: JSONObject = {
      val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T].map(identity[String])
      //some code, that uses fields, etc
      null
    } 

EDIT

Actually to get a non empty Set T needs an upper bound such as T <: Person ... and that is not what you wanted...

Leaving the answer here since the code does compile, and it might help someone in the direction of an answer

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