Introspect参数传递给Scala宏

时间:2013-01-14 19:32:16

标签: scala macros scala-macros

我想编写一个Scala宏,它将一个case类的实例作为参数。可以传递给宏的所有对象都必须实现特定的标记特征。

以下代码段显示了标记特征和实现它的两个示例案例类:

trait Domain
case class Country( id: String, name: String ) extends Domain
case class Town( id: String, longitude: Double, latitude: Double ) extends Domain

现在,我想使用宏编写以下代码,以避免运行时反射的严重性及其线程不安全:

object Test extends App {

  // instantiate example domain object
  val myCountry = Country( "CH", "Switzerland" )

  // this is a macro call
  logDomain( myCountry )
} 

logDomain在不同的项目中实现,看起来类似于:

object Macros {
  def logDomain( domain: Domain ): Unit = macro logDomainMacroImpl

  def logDomainMacroImpl( c: Context )( domain: c.Expr[Domain] ): c.Expr[Unit] = {
    // Here I would like to introspect the argument object but do not know how?
    // I would like to generate code that prints out all val's with their values
  }
}

宏的目的应该是生成代码 - 在运行时 - 输出给定对象的所有值(idname)并打印它们,如下所示:

id (String) : CH
name (String) : Switzerland

为了实现这一点,我必须动态检查传递的类型参数并确定其成员(vals)。然后我必须生成一个代表创建日志输出的代码的AST。无论实现标记特征“Domain”的特定对象传递给宏,宏都应该有效。

此时我迷路了。如果有人能给我一个起点或指向我一些文件,我将不胜感激?我对Scala相对较新,并且没有在Scala API文档或宏指南中找到解决方案。

2 个答案:

答案 0 :(得分:14)

列出案例类的访问器是一种常见的操作,当您使用宏时,我倾向于保留这样的方法:

def accessors[A: u.WeakTypeTag](u: scala.reflect.api.Universe) = {
  import u._

  u.weakTypeOf[A].declarations.collect {
    case acc: MethodSymbol if acc.isCaseAccessor => acc
  }.toList
}

这将为A提供trait ReflectionUtils { import scala.reflect.api.Universe def accessors[A: u.WeakTypeTag](u: Universe) = { import u._ u.weakTypeOf[A].declarations.collect { case acc: MethodSymbol if acc.isCaseAccessor => acc }.toList } def printfTree(u: Universe)(format: String, trees: u.Tree*) = { import u._ Apply( Select(reify(Predef).tree, "printf"), Literal(Constant(format)) :: trees.toList ) } } 的所有案例类访问器方法符号(如果有)。请注意,我在这里使用通用反射API - 不需要制作此宏特定的。

我们可以用其他一些方便的东西包装这个方法:

trait Domain

object Macros extends ReflectionUtils {
  import scala.language.experimental.macros
  import scala.reflect.macros.Context

  def log[D <: Domain](domain: D): Unit = macro log_impl[D]
  def log_impl[D <: Domain: c.WeakTypeTag](c: Context)(domain: c.Expr[D]) = {
    import c.universe._

    if (!weakTypeOf[D].typeSymbol.asClass.isCaseClass) c.abort(
      c.enclosingPosition,
      "Need something typed as a case class!"
    ) else c.Expr(
      Block(
        accessors[D](c.universe).map(acc =>
          printfTree(c.universe)(
            "%s (%s) : %%s\n".format(
              acc.name.decoded,
              acc.typeSignature.typeSymbol.name.decoded
            ),
            Select(domain.tree.duplicate, acc.name)
          )
        ),
        c.literalUnit.tree
      )
    )
  }
}

现在我们可以非常简洁地编写实际的宏代码:

scala> Macros.log(Town("Washington, D.C.", 38.89, 77.03))
id (String) : Washington, D.C.
longitude (Double) : 38.89
latitude (Double) : 77.03

请注意,我们仍然需要跟踪我们正在处理的特定案例类类型,但类型推断将在调用站点处理 - 我们不需要指定类型参数明确。

现在我们可以打开一个REPL,粘贴你的case类定义,然后编写以下内容:

scala> Macros.log(Country("CH", "Switzerland"))
id (String) : CH
name (String) : Switzerland

或者:

{{1}}

根据需要。

答案 1 :(得分:7)

从我所看到的,你需要解决两个问题:1)从宏参数中获取必要的信息,2)生成代表你需要的代码的树。

在Scala 2.10中,这些事情都是使用反射API完成的。按照Is there a tutorial on Scala 2.10's reflection API yet?查看可用的文档。

import scala.reflect.macros.Context
import language.experimental.macros

trait Domain
case class Country(id: String, name: String) extends Domain
case class Town(id: String, longitude: Double, latitude: Double) extends Domain

object Macros {
  def logDomain(domain: Domain): Unit = macro logDomainMacroImpl

  def logDomainMacroImpl(c: Context)(domain: c.Expr[Domain]): c.Expr[Unit] = {
    import c.universe._

    // problem 1: getting the list of all declared vals and their types
    //   * declarations return declared, but not inherited members
    //   * collect filters out non-methods
    //   * isCaseAccessor only leaves accessors of case class vals
    //   * typeSignature is how you get types of members
    //     (for generic members you might need to use typeSignatureIn)
    val vals = typeOf[Country].declarations.toList.collect{ case sym if sym.isMethod => sym.asMethod }.filter(_.isCaseAccessor)
    val types = vals map (_.typeSignature)

    // problem 2: generating the code which would print:
    // id (String) : CH
    // name (String) : Switzerland
    //
    // usually reify is of limited usefulness
    // (see https://stackoverflow.com/questions/13795490/how-to-use-type-calculated-in-scala-macro-in-a-reify-clause)
    // but here it's perfectly suitable
    // a subtle detail: `domain` will be possibly used multiple times
    // therefore we need to duplicate it
    val stmts = vals.map(v => c.universe.reify(println(
      c.literal(v.name.toString).splice +
      "(" + c.literal(v.returnType.toString).splice + ")" +
      " : " + c.Expr[Any](Select(domain.tree.duplicate, v)).splice)).tree)

    c.Expr[Unit](Block(stmts, Literal(Constant(()))))
  }
}