scalamacrosscala-2.11scala-quasiquotesscala-macro-paradise

Scala macro annotation - case class with type parameters


I'm trying to write a simple macro annotation for case classes which adds a method to the companion object. The catch is that the new method must account for type parameters on the annotated case class.

Here's the test that I need to pass

package my.macros

import org.scalatest._

class DefaultApplyTest extends FlatSpec with Matchers {

  @defaultApply case class Generic[A, B](a: A, b: B)

  it should "define defaultApply method in companion object" in {
    assert(Generic.defaultApply("str", 1) == Generic("str", 1))
  }
}

Here's the code I've written to accomplish this

package my.macros

import scala.reflect.macros._
import scala.language.experimental.macros
import scala.annotation.StaticAnnotation

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

object DefaultApply {

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

    def defaultApplyCompanion(classDecl: ClassDef) = {
      val (name, typeParams, valueParams) = try {
        val q"case class ${name: TypeName}[..${typeParams: Seq[TypeDef]}](..${valueParams: Seq[ValDef]}) extends ..$bases { ..$body }" = classDecl
        (name, typeParams, valueParams)
      } catch {
        case e: MatchError =>
          c.warning(c.enclosingPosition, e.toString)
          c.abort(c.enclosingPosition, "Annotation is only supported on case class")
      }

      val applyDef = q"""${name.toTermName}.apply[..$typeParams]"""
      c.warning(c.enclosingPosition, showRaw(applyDef))

      q"""
        object ${name.toTermName} {
          def defaultApply: (..${valueParams.map(_.tpt)}) => $name[..$typeParams] = $applyDef
        }
      """
    }

    def modifiedDeclaration(classDecl: ClassDef) = {
      val compDecl = defaultApplyCompanion(classDecl)

      c.Expr(q"""
        $classDecl
        $compDecl
      """)
    }

    annottees.map(_.tree) match {
      case (classDecl: ClassDef) :: Nil => modifiedDeclaration(classDecl)
      case _ => c.abort(c.enclosingPosition, "Invalid annottee")
    }
  }
}

The problem as I understand it is that when I try to lift the list of type parameters into the resulting syntax tree, they are not recognized as the same type parameters as from the original tree.

So what I'm focused on is that for this part of the macro

  val applyDef = q"""${name.toTermName}.apply[..$typeParams]"""
  c.warning(c.enclosingPosition, showRaw(applyDef))

The raw syntax tree is emitted as

TypeApply(Select(Ident(TermName("Generic")), TermName("apply")), List(TypeDef(Modifiers(PARAM), TypeName("A"), List(), TypeBoundsTree(EmptyTree, EmptyTree)), TypeDef(Modifiers(PARAM), TypeName("B"), List(), TypeBoundsTree(EmptyTree, EmptyTree))))

but the compiler isn't happy with this

type arguments [<notype>,<notype>] do not conform to method apply's type parameter bounds [A,B]

The ultimate use case is for generating instances of a cacheable type class which touches over 1k lines of code. The non-parameterized version already works, this is just the icing on the cake. There's something under the hood of scalac that I don't understand, but would like to. Your time spent reading this is greatly appreciated.

I'm using Scala 2.11.8 with macro paradise 2.1.0


Solution

  • The problem seems to be that you're using type parameters in the place of type arguments. This seems to work (I also had to add the type parameters to the defaultApply method declaration):

      val typeArgs = typeParams.map(_.name)
      val applyDef = q"""${name.toTermName}.apply[..$typeArgs]"""
      c.warning(c.enclosingPosition, showRaw(applyDef))
    
      q"""
        object ${name.toTermName} {
          def defaultApply[..$typeParams]:
              (..${valueParams.map(_.tpt)}) => $name[..$typeArgs] = $applyDef
        }
      """