scalatypeclassscala-macrosscala-2.13defmacro

Scala 2 macro type class derivation for `Coder[P <: Product]` ends with error `P does not take parameters`


I am a starter with Scala 2 Macros (before I switch to Dotty) who after trying out the shapeless type class derivation wanted to go one step beyond and write a macro that can generate a type class instances for any scala.Product without it.

(for the sake of example let's ignore nested recursive types, so my goal is flat case classes.)

My type class is an abstract class Coder[T] (e.g. trait with encode() / decode()).

So the generated code for:

case class Pojo(
  s: String,
  i: Int,
  l: List[Int]
)

should be something like:

import com.github.fpopic.scalamacros.Pojo
import org.apache.beam.sdk.coders.Coder
import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
import java.util

class PojoCoder extends Coder[Pojo] {

  import com.github.fpopic.scalamacros.beam.DefMacroCoder.{
    stringCoder,
    intCoder,
    listCoder
  }

  override def encode(value: Pojo, os: OutputStream): Unit = {
    stringCoder.encode(value.s, os)
    intCoder.encode(value.i, os)
    listCoder(intCoder).encode(value.l, os)
  }

  override def decode(is: InputStream): Pojo = {
    Pojo(
      s = stringCoder.decode(is),
      i = intCoder.decode(is),
      l = listCoder(intCoder).decode(is)
    )
  }

  override def getCoderArguments: util.List[_ <: Coder[_]] = {
    Collections.emptyList()
  }

  override def verifyDeterministic(): Unit =  ()
}

(removed fully specified class names to improve readability)

In the macro I try to:

def materializeProductCoder[P: c.WeakTypeTag](c: blackbox.Context): c.Expr[Coder[P]] = {
    import c.universe._
    val tpe = c.weakTypeOf[P]
    val helper = new MacrosHelper[c.type](c)

    val expressions =
      helper.getPrimaryConstructorMembers(tpe).map { field =>
        val fieldTerm = field.asTerm.name // e.g. value.s (for now just s)
        val fieldType = field.typeSignature.finalResultType // e.g. String

        val fieldCoderName = c.freshName(TermName("coder")) // e.g. give friendly name coder$...
        val fieldCoderInstance = // e.g. finds instance of Coder[String]
          c.typecheck(
            tree = q"""_root_.scala.Predef.implicitly[org.apache.beam.sdk.coders.Coder[${fieldType}]]""",
            silent = false
          )

        val fieldCoderExpression =
          q"private val ${fieldCoderName}: org.apache.beam.sdk.coders.Coder[${fieldType}] = ${fieldCoderInstance}"

        val fieldEncodeExpression =
          q"${fieldCoderName}.encode(value.${fieldTerm}, os)" // replace with full relative name (with dots) instead of value

        val fieldDecodeExpression =
          q"${field.asTerm} = ${fieldCoderName}.decode(is)"

        (fieldCoderExpression, fieldEncodeExpression, fieldDecodeExpression)
      }

    val fieldCodersExpression = expressions.map(_._1).distinct
    val coderEncodeExpresions = expressions.map(_._2)
    val coderDecodeExpresions = expressions.map(_._3)

    val coderExpression =
      q"""{
            new org.apache.beam.sdk.coders.Coder[${tpe}] {

              {import ${c.prefix}._}

              ..${fieldCodersExpression}

              override def encode(value: ${tpe}, os: java.io.OutputStream): _root_.scala.Unit = {
                ..${coderEncodeExpresions}
              }

              override def decode(is: java.io.InputStream): ${tpe} = {
                ${tpe.typeConstructor}(
                  ..${coderDecodeExpresions}
                )
              }

              override def getCoderArguments: java.util.List[_ <: org.apache.beam.sdk.coders.Coder[_]] = {
                java.util.Collections.emptyList
              }

              override def verifyDeterministic(): _root_.scala.Unit = ()
            }
          }
      """

    val ret = coderExpression
    c.Expr[Coder[P]](ret)
  }

But get an error after invoking sbt Test / compile:

(a bit struggling with imports and implicit search so for now having intermediate private vals, and distinct is useless)

{
  final class $anon extends org.apache.beam.sdk.coders.Coder[com.github.fpopic.scalamacros.beam.Pojo] {
    def <init>() = {
      super.<init>();
      ()
    };
    {
      import DefMacroCoder._;
      ()
    };
    private val coder$macro$1: org.apache.beam.sdk.coders.Coder[String] = scala.Predef.implicitly[org.apache.beam.sdk.coders.Coder[String]](DefMacroCoder.stringCoder);
    private val coder$macro$2: org.apache.beam.sdk.coders.Coder[Int] = scala.Predef.implicitly[org.apache.beam.sdk.coders.Coder[Int]](DefMacroCoder.intCoder);
    private val coder$macro$3: org.apache.beam.sdk.coders.Coder[List[Int]] = scala.Predef.implicitly[org.apache.beam.sdk.coders.Coder[List[Int]]](DefMacroCoder.listCoder[Int](DefMacroCoder.intCoder));
    override def encode(value: com.github.fpopic.scalamacros.beam.Pojo, os: java.io.OutputStream): _root_.scala.Unit = {
      coder$macro$1.encode(value.s, os);
      coder$macro$2.encode(value.i, os);
      coder$macro$3.encode(value.l, os)
    };
    override def decode(is: java.io.InputStream): com.github.fpopic.scalamacros.beam.Pojo = com.github.fpopic.scalamacros.beam.Pojo(s = coder$macro$1.decode(is), i = coder$macro$2.decode(is), l = coder$macro$3.decode(is));
    override def getCoderArguments: java.util.List[_$1] forSome { 
      <synthetic> type _$1 <: org.apache.beam.sdk.coders.Coder[_$2] forSome { 
        <synthetic> type _$2
      }
    } = java.util.Collections.emptyList;
    override def verifyDeterministic(): _root_.scala.Unit = ()
  };
  new $anon()
}
[error] .../DefMacroCoderSpec.scala:17:56: com.github.fpopic.scalamacros.beam.Pojo does not take parameters
[error]     val coder: Coder[Pojo] = DefMacroCoder.productCoder[Pojo]
[error]                                                        ^
[error] one error found

Which I believe comes from here but don't fully understand what the compiler is trying to tell me?

Link to the full code sample can be found here
Link to CI error can be found here.


Solution

  • The way you're instantiating your class is wrong:

    ${tpe.typeConstructor}(...)
    

    It should be

    new $tpe(...)
    

    or if you want to do it with case class companion object's apply instead of plain constructor:

    ${tpe.typeSymbol.companion}(...)
    

    NOTE: type constructor (also known as higher kinded type) has nothing to do with class constructor