Search code examples
scalamacrosreflect

How to create a Macro to create List of val in a case class?


I am trying to create a Macro to give me a list of val for a specific case class.

object CaseClass {

  def valList[T]: List[String] = macro implValList[T]

  def implValList[T](c: whitebox.Context): c.Expr[List[String]] = {
    import c.universe._

    val listApply = Select(reify(List).tree, TermName("apply"))

    val vals = weakTypeOf[T].decls.collect {
      case m: TermSymbol if m.isVal => q"${m.name}"
    }

    c.Expr[List[String]](Apply(listApply, vals.toList))
  }

}

So given

case class AClass(
   val a: String,
   val b: Int
)

I want a list of CaseClass.valList[AClass] = List("a", "b")


Solution

  • Not an expert on macros, so take it with a grain of salt. But I tested it with Intellij.

    First, to use weakTypeOf you need to take a WeakTypeTag as an implicit in your macro impl like this:

    def implValList[T](c: whitebox.Context)(implicit wt: c.WeakTypeTag[T]) ...
    

    Second, to create literals, you use this construct instead of your quasiquote, (which, I believe, actually does nothing):

    Literal(Constant(m.name.toString))
    

    Last, I recommend using this guard instead of isVal:

    m.isCaseAccessor && m.isGetter
    

    Which is properly checking for case class parameter and also being a getter (case class parameters are duplicated, one as isGetter, other one as isParam). The reason for this being that isVal names for case classes surprisingly produce a name ending in whitespace.

    The final implementation that works for me is as follows:

    object CaseClass {
    
      def valList[T]: List[String] = macro implValList[T]
    
      def implValList[T](c: whitebox.Context)(implicit wt: c.WeakTypeTag[T]): c.Expr[List[String]] = {
        import c.universe._
    
        val listApply = Select(reify(List).tree, TermName("apply"))
    
        val vals = weakTypeOf[T].decls.collect {
          case m: TermSymbol if m.isCaseAccessor && m.isGetter => Literal(Constant(m.name.toString))
        }
    
        c.Expr[List[String]](Apply(listApply, vals.toList))
      }
    
    }
    

    As an alternative (because macros are somewhat of a pain to set up - you cannot use macro in the same subproject that defines it), and you don't need it very often, you might be able to get away with a shapeless one-liner:

    import shapeless._
    import shapeless.ops.record.Keys
    
    case class Foo(a: Int, b: String)
    
    Keys[the.`LabelledGeneric[Foo]`.Repr].apply().toList.map(_.name) // List("a", "b")