12

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 ?

jilen
  • 5,633
  • 3
  • 35
  • 84

1 Answers1

16

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))
ghik
  • 10,706
  • 1
  • 37
  • 50
  • How about higher kind type? Eg. printMap[B, A[B]](a : A[B]) = println(asMap(a)) , it doesn't compile – jilen Dec 25 '13 at 08:07
  • Can you help me with higher kinded type – jilen Dec 25 '13 at 13:11
  • @jilen There's no higher kinded type used in your example. The method should work fine for case classes with type parameters, if this is what you meant. – ghik Dec 25 '13 at 18:18
  • I want to define a function def call[R,A[R] <:Req[R]: CaseClassToMap](req:A[R]) :R= doSomething(asMap(req)),the compiler complains A takes type parameter – jilen Dec 25 '13 at 23:20
  • @jilen Didn't you mean `def call[R,A <: Req[R]: CaseClassToMap](req: A): R` ? I don't see a reason to use higher kinds there. – ghik Dec 25 '13 at 23:29
  • define call that way the R is always inefered to `Nothing` – jilen Dec 26 '13 at 02:22
  • @jilen Hard to say anything without full example. I think you should ask a separate question about this. – ghik Dec 26 '13 at 12:12