Подтвердить что ты не робот

Аргумент Introspect передан макросу Scala

Я хотел бы запрограммировать макрос 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 with their values
  }
}

Цель макроса должна заключаться в создании кода, который во время выполнения выводит все значения (id и name) данного объекта и печатает их, как показано ниже:

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

Чтобы достичь этого, мне пришлось бы динамически проверять аргумент переданного типа и определять его члены (vals). Тогда мне пришлось бы генерировать AST, представляющий код, который создает выход журнала. Макрос должен работать независимо от того, какой конкретный объект реализует маркерный признак "Домен" передается макросу.

В этот момент я проиграл. Я был бы признателен, если бы кто-то мог дать мне отправную точку или указать мне какую-то документацию? Я относительно новичок в Scala и не нашел решения в документах API Scala API или Макро.

4b9b3361

Ответ 1

Список аксессуаров класса case является такой общей операцией, когда вы работаете с макросами, которые я обычно придерживаюсь таким способом:

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
}

Это даст нам все символы метода доступа класса case для A, если он есть. Обратите внимание, что я использую API общего отражения здесь - нет необходимости делать эту макро-специфическую.

Мы можем обернуть этот метод с помощью других удобных вещей:

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
    )
  }
}

И теперь мы можем написать фактический макрокод довольно кратко:

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
      )
    )
  }
}

Обратите внимание, что нам по-прежнему нужно отслеживать тип конкретного класса case, с которым мы имеем дело, но вывод типа позаботится об этом на сайте вызова - нам не нужно явно указывать параметр типа.

Теперь мы можем открыть REPL, вставить определения класса case и записать следующее:

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

Или:

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

По желанию.

Ответ 2

Из того, что я вижу, вам нужно решить две проблемы: 1) получить необходимую информацию из аргумента макроса, 2) генерировать деревья, которые представляют нужный вам код.

В Scala 2.10 все это делается с помощью API отражения. Следуйте Есть ли еще учебник по API Scala 2.10 отражения?, чтобы узнать, какая документация доступна для него.

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/info/13795490/how-to-use-type-calculated-in-scala-macro-in-a-reify-clause)
    // but here it 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(()))))
  }
}