Макросы Scala: создание карты из полей класса в Scala

предположим, что у меня много подобных классов данных. Вот пример класса User, который определяется следующим образом:

case class User (name: String, age: Int, posts: List[String]) {
  val numPosts: Int = posts.length

  ...

  def foo = "bar"

  ...
}

меня интересует автоматическое создание метода (во время компиляции), которая возвращает Map таким образом, что каждое имя поля сопоставляется с его значением при вызове во время выполнения. Для примера выше предположим, что мой метод называется toMap:

val myUser = User("Foo", 25, List("Lorem", "Ipsum"))

myUser.toMap

должен возвратить

Map("name" -> "Foo", "age" -> 25, "posts" -> List("Lorem", "Ipsum"), "numPosts" -> 2)

как вы бы сделали это с помощью макросов?

вот что я сделал: во-первых, я создал Model класс как суперкласс для всех моих классов данных и реализован метод там, как это:

abstract class Model {
  def toMap[T]: Map[String, Any] = macro toMap_impl[T]
}

class User(...) extends Model {
  ...
}

затем я определил реализацию макроса в отдельном

2 ответов


обратите внимание, что это можно сделать гораздо более элегантно без toString / c.parse бизнес:

import scala.language.experimental.macros

abstract class Model {
  def toMap[T]: Map[String, Any] = macro Macros.toMap_impl[T]
}

object Macros {
  import scala.reflect.macros.Context

  def toMap_impl[T: c.WeakTypeTag](c: Context) = {
    import c.universe._

    val mapApply = Select(reify(Map).tree, newTermName("apply"))

    val pairs = weakTypeOf[T].declarations.collect {
      case m: MethodSymbol if m.isCaseAccessor =>
        val name = c.literal(m.name.decoded)
        val value = c.Expr(Select(c.resetAllAttrs(c.prefix.tree), m.name))
        reify(name.splice -> value.splice).tree
    }

    c.Expr[Map[String, Any]](Apply(mapApply, pairs.toList))
  }
}

обратите внимание также, что вам нужно c.resetAllAttrs бит, Если вы хотите иметь возможность написать следующее:

User("a", 1, Nil).toMap[User]

без него вы получите запутанный ClassCastException в этой ситуации.

кстати, вот трюк, который я использовал, чтобы избежать дополнительного параметра типа, например user.toMap[User] при написании макросов вроде этого:

import scala.language.experimental.macros

trait Model

object Model {
  implicit class Mappable[M <: Model](val model: M) extends AnyVal {
    def asMap: Map[String, Any] = macro Macros.asMap_impl[M]
  }

  private object Macros {
    import scala.reflect.macros.Context

    def asMap_impl[T: c.WeakTypeTag](c: Context) = {
      import c.universe._

      val mapApply = Select(reify(Map).tree, newTermName("apply"))
      val model = Select(c.prefix.tree, newTermName("model"))

      val pairs = weakTypeOf[T].declarations.collect {
        case m: MethodSymbol if m.isCaseAccessor =>
          val name = c.literal(m.name.decoded)
          val value = c.Expr(Select(model, m.name))
          reify(name.splice -> value.splice).tree
      }

      c.Expr[Map[String, Any]](Apply(mapApply, pairs.toList))
    }
  }
}

теперь мы можем написать следующее:

scala> println(User("a", 1, Nil).asMap)
Map(name -> a, age -> 1, posts -> List())

и не нужно указывать, что мы говорим о User.


есть отличный блог на карте в/из преобразования класса case с помощью макросов.