Scala Macros: Creating a Map from Class Fields in Scala

Let's say that I have many similar data classes. Here is an example of the User class, which is defined as follows:

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

I'm interested in automatically creating a method ( at compile time ) that returns a Map in such a way that each field name matches its value when it is called at run time. In the example above, suppose my method is called toMap :

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

must return

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

How would you do this with macros?

Here's what I did: first, I created the Model class as a superclass for all my data classes and implemented this method:

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

Then I defined the macro implementation in a separate Macros object:

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

But I get this error from sbt when I try to compile it:

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

Macros are first collected. scala. Here is a snippet from my 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) 

What am I doing wrong? I think the compiler is also trying to evaluate the field identifiers when it creates the expression, but I don’t know how to correctly return them in the expression. Could you show me how to do this?

Thank you very well in advance.

+29
scala case-class scala-macros
Jun 20 '13 at 20:30
source share
2 answers

Please note that this can be done much more elegantly without the 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)) } } 

Note that you need the c.resetAllAttrs bit if you want to write the following:

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

Without this, you will encounter the intricacies of ClassCastException in this situation.

By the way, here is a trick I used to avoid an extra type parameter, for example. user.toMap[User] when writing macros:

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

Now we can write the following:

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

And do not indicate that we are talking about User .

+31
Jun 20 '13 at 21:45
source share

great blog post on map to / from case class conversion using macros.

+9
Dec 03 '14 at 9:00
source share



All Articles