flavian flavian - 2 months ago 14
Scala Question

Scala macros: Emit for comprehensions from macros

Trying to emit a

for yield
block from a blackbox macro, but I'm failing to understand how you can create the block with valid syntax.

So below
source
is a hardcoded param name as this block is later inserted inside a method that will have the matching param name.
params
is just
params: Seq[c.universe.ValDef]
, enclosing the
case class
fields.

def extract(source: Source): Option[CaseClass] = { ... }

val extractors = accessors(c)(params) map {
case (nm, tpe) => {
val newTerm = TermName(nm.toString + "Opt")
q"""$newTerm <- DoStuff[$tpe].apply("$nm", source)"""
}
}

val extractorNames = accessors(c)(params) map {
case (nm, tpe) => TermName(nm.toString + "Opt")
}


This is basically taking a
case class
, and outputting a for yield black to basically recreate the case class from a comprehension.

Every field in the case class of the form
name: Type
is transformed to a set of extractors that yield the same case class instance back if the for comprehension is successful.

case class Test(id: Int, text: String)


Will be macro transformed to the following, where
Extract
is just a type class and
Extract.apply[T : Extract]
is just materialising the context bound with
implicitly[Extract[T]]
:

for {
idOpt <- Extract[Int].apply("id", source): Option[Int]
textOpt <- Extract[String].apply("text", source): Option[String]
} yield Test(idOpt, textOpt)


The problem comes in having to quote the inner for yield expressions with and output
a <- b
blocks.

def extract(source: Source): Option[$typeName] = {
for {(..$extractors)} yield $companion.apply(..$extractorNames)
}


The error is
';' expected but '<-' found
, which is pretty obvious as
a <- b
is invalid Scala by itself. What is the correct way to generate and quasiquote the expression block such that the above would work?

Answer

Here you can see all the different kinds of quasiquotes. There you can see that to express the a <- b syntax you need the fq interpolator. So that code will probably become:

val extractors = accessors(c)(params) map {
  case (nm, tpe) => {
    val newTerm = TermName(nm.toString + "Opt")
    fq"""$newTerm <- DoStuff[$tpe].apply("$nm", source)"""
  }
}

And then with the normal interpolator:

q"for {(..$extractors)} yield $companion.apply(..$extractorNames)"