Let\'s say that I have a lot of similar data classes. Here\'s an example class User
which is defined as follows:
case class User (name: String,
Note that this can be done much more elegantly without the toString
/ c.parse
business:
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 also that you need the c.resetAllAttrs
bit if you want to be able to write the following:
User("a", 1, Nil).toMap[User]
Without it you'll get a confusing ClassCastException
in this situation.
By the way, here's a trick that I've used to avoid the extra type parameter in e.g. user.toMap[User]
when writing macros like this:
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 don't need to specify that we're talking about a User
.
There is an excellent blog post on map to/from case class conversion using macros.
Starting Scala 2.13
, case class
es (which are an implementation of Product) are now provided with a productElementNames method which returns an iterator over their field's names.
By zipping field names with field values obtained with productIterator one can obtained a Map
out of whatever case class:
// val user = User("Foo", 25, List("Lorem", "Ipsum"))
(user.productElementNames zip user.productIterator).toMap
// Map[String, Any] = Map("name" -> "Foo", "age" -> 25, "posts" -> List("Lorem", "Ipsum"))