Is it possible to replace the default apply method generated for case classes in a Scala macro?

346 Views Asked by At

It seems like this doesn't work ( Using 2.11.1 and macro paradise 2.0.1). I was hoping that the case class generated methods would either be suppressed, or be in the tree so I could get rid of it. Is this a hard limitation?

class evis extends StaticAnnotation {
  def macroTransform(annottees: Any*) = macro EvisMacro.impl
}

object EvisMacro {

  def impl(c: blackbox.Context)(annottees: c.Expr[Any]*) : c.Expr[Any] = {
    import c.universe._

    def makeApply(tpName: TypeName, parents: List[Tree], params: List[List[ValDef]] ) : List[Tree]= {
      List(q"""def apply(...$params): $tpName = null""")
    }

    val result = annottees map (_.tree) match {
      case (classDef @ q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }")
        :: Nil if mods.hasFlag(Flag.CASE) =>
        c.info(c.enclosingPosition,  s"Eviscerating $tpname !($mods, $parents, $paramss)", true)

        parents match {
          case q"${pname: TypeName}" :: rest =>
            c.info(c.enclosingPosition, s"${pname.decodedName}", true )
            val sc = c.universe.rootMirror.staticClass( pname.decodedName.toString  )
            c.info(c.enclosingPosition, s"${sc}", true )
        }

        val name = tpname.toTermName
        q"""
        $classDef
        object $name {
          ..${makeApply(tpname, parents, paramss)}
        }
        """
      case (classDef @ q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }")
        :: q"object $objName {..$objDefs}"
        :: Nil if mods.hasFlag(Flag.CASE) =>
        q"""
        $classDef
         object $objName {
           ..${makeApply(tpname, parents, paramss)}
           ..$objDefs
         }
         """
      case _ => c.abort(c.enclosingPosition, "Invalid annotation target: must be a case class")
    }
    c.Expr[Any](result)
  }

}

Using it:

trait Thing
@evis
case class Trade(id: Long, notional: Long, comment: String) extends Thing
@evis
case class Pop(name: String) extends Thing

object Pop{

}

object TestTrade extends App{

  val t = Trade (1, 1, "")
  val p : Pop = Pop("")

  println(t)

}

Results in:

Error:(2, 2) method apply is defined twice conflicting symbols both originated in file 'core/src/main/scala/Test.scala' @evis ^

1

There are 1 best solutions below

0
On

The problem is caused by the fact that, to the compiler, code generated by macro annotations isn't any different from code written by hand. If you manually write the code produced by the macro provided in the example, you'll get exactly the same double definition error, so it's not a bug - it's a limitation of case class synthesis. Unfortunately, case class synthesis is not extensible, so this needs to be worked around.

One workaround that I'd propose is erasing the CASE flag from the mods of the class, and then the macro can be completely free in choosing which members to generate. This, however, means that the macro will have to generate all the code that case classes typically generate, which is not going to be very pleasant. Another caveat here is that the compiler treats pattern matching over CASE classes specially by emitting somewhat more efficient code, so such an emulation would also lose some performance (I'd reckon that the loss will be miniscule, and probably even non-existent with the new name-based pattern matching mechanism from Scala 2.11 - but that would need to be tested).