Reputation: 4685
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
Upvotes: 3
Views: 524
Reputation: 51703
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())
}
}
Upvotes: 3
Reputation: 15464
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 e.g. Get field names list from case class
Upvotes: 2
Reputation: 4779
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
Upvotes: 0