3
votes

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

3

3 Answers

3
votes

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())
  }
}
2
votes

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

0
votes

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