scala case-class scala-macros

Scala Macros: hacer un mapa fuera de los campos de una clase en Scala



case-class scala-macros (2)

Existe una excelente publicación de blog en el mapa a / desde la conversión de clase de caso utilizando macros.

Digamos que tengo muchas clases de datos similares. Aquí hay un User clase de ejemplo que se define de la siguiente manera:

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

Estoy interesado en crear automáticamente un método ( en tiempo de compilación ) que devuelva un Map de forma que cada nombre de campo se correlacione con su valor cuando se llame en tiempo de ejecución. Para el ejemplo anterior, digamos que mi método se llama toMap :

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

debería regresar

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

¿Cómo harías esto con macros?

Esto es lo que he hecho: Primero, creé una clase Model como una superclase para todas mis clases de datos e implementé el método allí de esta manera:

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

Luego definí una implementación de macro en un objeto Macros separado:

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 <- members val fieldString = Literal(Constant(m.toString.replace("value ", ""))) val field = Ident(m) } yield (fieldString, field) val mappings = tuples.toMap /* Parse the string version of the map [i.e. Map("posts" -> (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)) } }

Sin embargo, obtengo este error de sbt cuando intento compilarlo:

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

Macros.scala se está compilando primero. Aquí está el fragmento de mi 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)

¿Qué estoy haciendo mal? Creo que el compilador también intenta evaluar los identificadores de campo cuando crea la expresión, pero no sé cómo devolverlos correctamente en la expresión. ¿Podrías enseñarme cómo hacer eso?

Muchas gracias de antemano.


Tenga en cuenta que esto se puede hacer mucho más elegante sin el negocio 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)) } }

Tenga en cuenta también que necesita el bit c.resetAllAttrs si desea poder escribir lo siguiente:

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

Sin él, obtendrás una ClassCastException confusa en esta situación.

Por cierto, he aquí un truco que he usado para evitar el parámetro de tipo extra en, por ejemplo, user.toMap[User] cuando escribo macros como este:

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

Ahora podemos escribir lo siguiente:

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

Y no necesita especificar que estamos hablando de un User .