How to use scala macros to create a function objec

2020-02-12 21:47发布

问题:

I am trying to use Scala macros to create a case class map of single-parameter copy methods, with each method accepting a Play Json JsValue and a case class instance, and returning an updated copy of the instance. However, I am running into problems with the macro syntax for returning a function object.

Given a case class

case class Clazz(id: Int, str: String, strOpt: Option[String])

the intention is to create a map of the class's copy methods

implicit def jsonToInt(json: JsValue) = json.as[Int]
implicit def jsonToStr(json: JsValue) = json.as[String]
implicit def jsonToStrOpt(json: JsValue) = json.asOpt[String]

Map("id" -> (json: JsValue, clazz: Clazz) = clazz.copy(id = json),
  "str" -> (json: JsValue, clazz: Clazz) = clazz.copy(str = json), ...)

I have found two related questions:

Using macros to create a case class field map: Scala Macros: Making a Map out of fields of a class in Scala

Accessing the case class copy method using a macro: Howto model named parameters in method invocations with Scala macros?

...but I am stuck at how I can create a function object so that I can return a Map[String, (JsValue, T) => T]


Edit: Thanks to Eugene Burmako's suggestion to use quasiquotes - this is where I'm currently at using Scala 2.11.0-M7, basing my code on Jonathan Chow's post (I switched from using (T, JsValue) => T to (T, String) => T to simplify my REPL imports)

Edit2: Now incorporating $tpe splicing

import scala.language.experimental.macros

implicit def strToInt(str: String) = str.toInt

def copyMapImpl[T: c.WeakTypeTag](c: scala.reflect.macros.Context): 
    c.Expr[Map[String, (T, String) => T]] = {

  import c.universe._

  val tpe = weakTypeOf[T]

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

  val methods = fields.map { field => {
    val name = field.name
    val decoded = name.decoded
    q"{$decoded -> {(t: $tpe, str: String) => t.copy($name = str)}}"
  }}

  c.Expr[Map[Sring, (T, String) => T]] {
    q"Map(..$methods)"
  }
}

def copyMap[T]: Map[String, (T, String) => T] = macro copyMapImpl[T]

case class Clazz(i: Int, s: String)

copyMap[Clazz]

回答1:

You got almost everything right in your code, except for the fact that you need to splice T into a quasiquote, i.e. to write $tpe instead of just T.

For that to look more natural, I usually explicitly declare type tag evidences in macros, e.g. def foo[T](c: Context)(implicit T: c.WeakTypeTag[T]) = .... After that I just write $T, and it looks almost fine :)

You might ask why quasiquotes can't just figure out that in the place where they're written T refers to the type parameter of a macro and then automatically splice it in. That would be very reasonable question, actually. In languages like Racket and Scheme, quasiquotes are smart enough to remember things about the lexical context they are written in, but in Scala this is a bit more difficult, because there are so many different scopes in the language. Yet, there's a plan to get there, and research in that direction in already underway: https://groups.google.com/forum/#!topic/scala-language/7h27npd1DKI.