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

Предположим, что у меня много подобных classов данных. Вот пример classа 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) 

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

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

 abstract class Model { def toMap[T]: Map[String, Any] = macro toMap_impl[T] } class User(...) extends Model { ... } 

Затем я определил реализацию макроса в отдельном объекте Macros :

 object Macros { import scala.language.experimental.macros import scala.reflect.macros.Context def getMap_impl[T: c.WeakTypeTag](c: Context): c.Expr[Map[String, Any]] = { import c.universe._ val tpe = weakTypeOf[T] // Filter members that start with "value", which are val fields val members = tpe.members.toList.filter(m => !m.isMethod && m.toString.startsWith("value")) // Create ("fieldName", field) tuples to construct a map from field names to fields themselves val tuples = for { m  (posts), "age" -> (age), "name" -> (name))] to get the AST * for the map, which is generated as: * * Apply(Ident(newTermName("Map")), * List( * Apply(Select(Literal(Constant("posts")), newTermName("$minus$greater")), List(Ident(newTermName("posts")))), * Apply(Select(Literal(Constant("age")), newTermName("$minus$greater")), List(Ident(newTermName("age")))), * Apply(Select(Literal(Constant("name")), newTermName("$minus$greater")), List(Ident(newTermName("name")))) * ) * ) * * which is equivalent to Map("posts".$minus$greater(posts), "age".$minus$greater(age), "name".$minus$greater(name)) */ c.Expr[Map[String, Any]](c.parse(mappings.toString)) } } 

Тем не менее, я получаю эту ошибку от sbt, когда пытаюсь ее скомпилировать:

 [error] /Users/emre/workspace/DynamoReflection/core/src/main/scala/dynamo/Main.scala:9: not found: value posts [error] foo.getMap[User] [error] ^ 

Сначала выполняется assembly макросов. Вот fragment из моего Build.scala:

 lazy val root: Project = Project( "root", file("core"), settings = buildSettings ) aggregate(macros, core) lazy val macros: Project = Project( "macros", file("macros"), settings = buildSettings ++ Seq( libraryDependencies <+= (scalaVersion)("org.scala-lang" % "scala-reflect" % _)) ) lazy val core: Project = Project( "core", file("core"), settings = buildSettings ) dependsOn(macros) 

Что я делаю не так? Я думаю, что компилятор также пытается оценить идентификаторы полей, когда он создает выражение, но я не знаю, как правильно их вернуть в выражении. Не могли бы вы показать мне, как это сделать?

Большое спасибо заранее.

    Обратите внимание, что это можно сделать гораздо более элегантно без бизнеса 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 .

    Существует отличное сообщение в блоге на карте в / из преобразования classа case с использованием макросов.

    Давайте будем гением компьютера.