简体   繁体   中英

Class type required but E found (scala macro)

I'm trying to remove some of the boilerplate in an API I am writing.

Roughly speaking, my API currently looks like this:

def toEither[E <: WrapperBase](priority: Int)(implicit factory: (String, Int) => E): Either[E, T] = {
  val either: Either[String, T] = generateEither()
  either.left.map(s => factory(s, priority))
}

Which means that the user has to generate an implicit factory for every E used. I am looking to replace this with a macro that gives a nice compile error if the user provided type doesn't have the correct ctor parameters.

I have the following:

object GenericFactory {
  def create[T](ctorParams: Any*): T = macro createMacro[T]

  def createMacro[T](c: blackbox.Context)(ctorParams: c.Expr[Any]*)(implicit wtt: WeakTypeType[T]): c.Expr[T] = {
    import c.universe._
    c.Expr[T](q"new $wtt(..$ctorParams)")
  }
}

If I provide a real type to this GenericFactory.create[String]("hey") I have no issues, but if I provide a generic type: GenericFactory.create[E]("hey") then I get the following compile error: class type required by E found .

Where have I gone wrong? Or if what I want is NOT possible, is there anything else I can do to reduce the effort for the user?

Sorry but I don't think you can make it work. The problem is that Scala (as Java) uses types erasure. It means that there is only one type for all generics kinds (possibly except for value-type specializations which is not important now). It means that the macro is expanded only once for all E rather then one time for each E specialization provided by the user. And there is no way to express a restriction that some generic type E must have a constructor with a given signature (and if there were - you wouldn't need you macro in the first place). So obviously it can not work because the compiler can't generate a constructor call for a generic type E . So what the compiler says is that for generating a constructor call it needs a real class rather than generic E .

To put it otherwise, macro is not a magic tool. Using macro is just a way to re-write a piece of code early in the compiler processing but then it will be processed by the compiler in a usual way. And what your macro does is rewrites

GenericFactory.create[E]("hey")

with something like

new E("hey")

If you just write that in your code, you'll get the same error (and probably will not be surprised).

I don't think you can avoid using your implicit factory. You probably could modify your macro to generate those implicit factories for valid types but I don't think you can improve the code further.


Update: implicit factory and macro

If you have just one place where you need one type of constructors I think the best you can do (or rather the best I can do ☺) is following:

Sidenote the whole idea comes from "Implicit macros" article

  1. You define StringIntCtor[T] typeclass trait and a macro that would generate it:
import scala.language.experimental.macros
import scala.reflect.macros._


trait StringIntCtor[T] {
  def create(s: String, i: Int): T
}


object StringIntCtor {
  implicit def implicitCtor[T]: StringIntCtor[T] = macro createMacro[T]


  def createMacro[T](c: blackbox.Context)(implicit wtt: c.WeakTypeTag[T]): c.Expr[StringIntCtor[T]] = {
    import c.universe._
    val targetTypes = List(typeOf[String], typeOf[Int])
    def testCtor(ctor: MethodSymbol): Boolean = {
      if (ctor.paramLists.size != 1)
        false
      else {
        val types = ctor.paramLists(0).map(sym => sym.typeSignature)
        (targetTypes.size == types.size) && targetTypes.zip(types).forall(tp => tp._1 =:= tp._2)
      }
    }

    val ctors = wtt.tpe.decl(c.universe.TermName("<init>"))
    if (!ctors.asTerm.alternatives.exists(sym => testCtor(sym.asMethod))) {
      c.abort(c.enclosingPosition, s"Type ${wtt.tpe} has no constructor with signature <init>${targetTypes.mkString("(", ", ", ")")}")
    }


    // Note that using fully qualified names for all types except imported by default are important here
    val res = c.Expr[StringIntCtor[T]](
      q"""
           (new so.macros.StringIntCtor[$wtt] {
               override def create(s:String, i: Int): $wtt = new $wtt(s, i)
             })
       """)

    //println(res) // log the macro
    res


  }
}
  1. You use that trait as
class WrapperBase(val s: String, val i: Int)

case class WrapperChildGood(override val s: String, override val i: Int, val float: Float) extends WrapperBase(s, i) {
  def this(s: String, i: Int) = this(s, i, 0f)
}

case class WrapperChildBad(override val s: String, override val i: Int, val float: Float) extends WrapperBase(s, i) {
}

object EitherHelper {
  type T = String

  import scala.util._

  val rnd = new Random(1)

  def generateEither(): Either[String, T] = {
    if (rnd.nextBoolean()) {
      Left("left")
    }
    else {
      Right("right")
    }
  }


  def toEither[E <: WrapperBase](priority: Int)(implicit factory: StringIntCtor[E]): Either[E, T] = {
    val either: Either[String, T] = generateEither()
    either.left.map(s => factory.create(s, priority))
  }
}

So now you can do:

val x1 = EitherHelper.toEither[WrapperChildGood](1)
println(s"x1 = $x1")
val x2 = EitherHelper.toEither[WrapperChildGood](2)
println(s"x2 = $x2")
//val bad = EitherHelper.toEither[WrapperChildBad](3) // compilation error generated by c.abort

and it will print

x1 = Left(WrapperChildGood(left,1,0.0))
x2 = Right(right)

If you have many different places where you want to ensure different constructors exists, you'll need to make the macro much more complicated to generate constructor calls with arbitrary signatures passed from the outside.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM