1

I'm trying to write a macro that that can use information about fields of a class to generate a schema. For example, let's say I have a typeclass called SchemaWriter[T] for which I wish to generate implementations using a macro.

trait SchemaWriter[T] {
  def schema: org.bibble.MsonSchema
}

case class MsonSchema(fields: Seq[MsonType])
case class MsonType(name: String, `type`: Class[_]) // might want other stuff here too, derived from a symbol or type-signature
object MsonType {
  def apply(symbol:Symbol): MsonType = ... 
}

The idea is that my macro would spit out code similar to:

class FooSchemaWriter extends SchemaWriter[Foo] {
  def schema : org.bibble.MsonSchema= {
   val fields = for (symbol <- fields of class) yield {
    MsonType(symbol)
   }
   org.bibble.MsonSchema(fields)
 }
}

I can implement a macro such as:

object Macros {

  def writerImpl[T: c.WeakTypeTag](c: Context): c.Expr[SchemaWriter[T]] = {

    import c.universe._
    val T = weakTypeOf[T]

    val fields = T.declarations.collectFirst {
      case m: MethodSymbol if m.isPrimaryConstructor => m
    }.get.paramss.head

    val fieldTrees: [Tree] = fields.map { f =>
      q"""org.bibble.MsonType(${f})"""
    }

    c.Expr[SchemaWriter[T]]( q"""
      new SchemaWriter[$T] {
        def schema = {
         val fields = Seq(..$fieldTrees)
         org.bibble.MsonSchema(fields)
        }
      }
    """)
  }
}

But creating the qqs for the fields causes either a liftable error or an unquote error. I asked a similar question yesterday Scala macros Type or Symbol lifted which received a fantastic answer, but my implementation can't be limited to passing Strings about as I need more type information to generate details for the schema, which is where I think my confusion lies.

Community
  • 1
  • 1
sksamuel
  • 16,154
  • 8
  • 60
  • 108
  • It's nearly impossible to help without knowing the definition of `Field`. In particular, what is the signature of `Field`'s constructor. Generally speaking, when posting a question you should strive to post a [minimal, complete, and verifiable example](http://stackoverflow.com/help/mcve). Yes, I know that you cannot actually post an example that compiles given that you are precisely asking for how to fix your code, but at least provide all the required dependencies. – Régis Jean-Gilles Sep 18 '15 at 08:06
  • I will update with a proper definition of Schema and Field – sksamuel Sep 18 '15 at 12:32
  • Updated for concrete schema type. What goes into this Mson schema isn't the key bit, but if I can get what's up there now to compile, the rest should be easy. – sksamuel Sep 18 '15 at 12:38

1 Answers1

1

I think that your main confusion is that you don't seem to entirely get how macros are processed. It is entirely compile time. When your macro runs, it has access to information about the compiled program, including Symbols. The macro then generates some code that will become part of your program, but that generated code itself has not annymore access to the Symbols or anything else that the macro had access to.

So MsonType.apply in your example is not going to be of any use, because the input is a Symbol which is available in the macro only (at compile-time), and the output is a MsonSchema, which you need at run-time. What would make sense is to change the return type from MsonType to c.Expr[MsonType] (or simply c.universe.Tree), or in other words MsonType.apply would now take a symbol and return a tree representing the MsonType instance (as opposed to returning an MsonType instance), and your macro could then call that and include it in the final tree returned to the compiler (which the compiler will then include in your program at the call site). In this particular case, I think it's better to just remove MsonType.apply altogether, and implement the conversion from Symbol to c.universe.Tree right in the writerImpl macro.

This conversion is actually very easy. All you need to construct MsonType is the field name, and a Class instance, so there you go:

q"""org.bibble.MsonType(${f.name.decoded}, classOf[${f.typeSignature}])"""

For a field foo of type Bar this will generate a tree representing the following expression:

org.bibble.MsonType("foo", classOf[Bar])

So that's it, we're good to go.

You'll find a full implementation below. Note that I've taken the liberty to change the definition of your schema types in a way that is far more logical and versatile to me. And in particular, each field now has its associated schema (given your previous question, this is clearly what you wanted in the first place).

scala> :paste
// Entering paste mode (ctrl-D to finish)

import scala.language.experimental.macros
import scala.reflect.macros._

case class MsonSchema(`type`: Class[_], fields: Seq[MsonField])
case class MsonField(name: String, schema: MsonSchema)
trait SchemaWriter[T] {
  def schema: MsonSchema
}
object SchemaWriter {
  def apply[T:SchemaWriter]: SchemaWriter[T] = implicitly
  implicit def defaultWriter[T] = macro Macros.writerImpl[T]
}
object Macros {
  def writerImpl[T: c.WeakTypeTag](c: Context): c.Expr[SchemaWriter[T]] = {
    import c.universe._
    c.Expr[SchemaWriter[T]](generateSchemaWriterTree(c)(weakTypeOf[T]))
  }
  private def generateSchemaWriterTree(c: Context)(T: c.universe.Type): c.universe.Tree = {
    import c.universe._
    val fields = T.declarations.collectFirst {
      case m: MethodSymbol if m.isPrimaryConstructor => m
    }.get.paramss.head

    val MsonFieldSym = typeOf[MsonField].typeSymbol
    val SchemaWriterSym = typeOf[SchemaWriter[_]].typeSymbol
    val fieldTrees: Seq[Tree] = fields.map { f =>
      q"""new $MsonFieldSym(
        ${f.name.decoded},
        _root_.scala.Predef.implicitly[$SchemaWriterSym[${f.typeSignature}]].schema
      )"""
    }

    c.resetLocalAttrs(q"""
      new $SchemaWriterSym[$T] {
        val schema = MsonSchema(classOf[$T], Seq(..$fieldTrees))
      }
    """)
  }
}

// Exiting paste mode, now interpreting.

warning: there were 7 deprecation warnings; re-run with -deprecation for details
warning: there was one feature warning; re-run with -feature for details
import scala.language.experimental.macros
import scala.reflect.macros._
defined class MsonSchema
defined class MsonField
defined trait SchemaWriter
defined object SchemaWriter
defined object Macros

scala> case class Foo(ab: String, cd: Int)
defined class Foo

scala> SchemaWriter[Foo].schema
res0: MsonSchema = MsonSchema(class Foo,List(MsonField(ab,MsonSchema(class java.lang.String,List())), MsonField(cd,MsonSchema(int,List()))))
Régis Jean-Gilles
  • 32,541
  • 5
  • 83
  • 97