11
votes

I define following macro to transform case fields to map

   import scala.language.experimental.macros
    import scala.reflect.macros.Context

    def asMap_impl[T: c.WeakTypeTag](c: Context)(t: c.Expr[T]) = {
      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(t.tree, m.name))
          reify(name.splice -> value.splice).tree
      }

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

And the method implementation

def asMap[T](t: T) = macro asMap_impl[T]

Then I define a case class to test it

case class User(name : String)

It works fine(with scala repl):

 scala> asMap(User("foo")) res0:
 scala.collection.immutable.Map[String,String] = Map(name -> foo)

But When I wrap this method with another generic method

def printlnMap[T](t: T) = println(asMap(t))

This method always print empty map:

scala> printlnMap(User("foo"))
Map()

The type information seems lost, how to get the printlnMap to print all fields ?

1

1 Answers

15
votes

The reason why this doesn't work is that your macro will be called only once - when compiling printlnMap function. This way it will see T as an abstract type. The macro will not be called on each invocation of printlnMap.

One way to quickly fix this is to implement printlnMap also as a macro. Of course, this is not ideal. So, here's a different approach - materialization of typeclass instances:

First, define a typeclass that will allow us to convert case class instances to maps:

trait CaseClassToMap[T] {
  def asMap(t: T): Map[String,Any]
}

Then, implement a macro that will materialize an instance of this type class for some case class T. You can put it into CaseClassToMap companion object so that it is visible globally.

object CaseClassToMap {
  implicit def materializeCaseClassToMap[T]: CaseClassToMap[T] = macro impl[T]

  def impl[T: c.WeakTypeTag](c: Context): c.Expr[CaseClassToMap[T]] = {
    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(Ident(newTermName("t")), m.name))
        reify(name.splice -> value.splice).tree
      }

    val mapExpr = c.Expr[Map[String, Any]](Apply(mapApply, pairs.toList))

    reify {
      new CaseClassToMap[T] {
        def asMap(t: T) = mapExpr.splice
      }
    }
  }
}

Now, you can do this:

def asMap[T: CaseClassToMap](t: T) =
  implicitly[CaseClassToMap[T]].asMap(t)

def printlnMap[T: CaseClassToMap](t: T) =
  println(asMap(t))