Search code examples
scalareflectionscala-quasiquotes

Insert string into quasiquote before it's interpolated (runtime quasiquote?)


I'm trying to generate Tree for function that accepts case class value and returns value of case class parameter at given position. This is useful for extracting values of private parameters.

  import reflect.runtime.currentMirror
  import scala.reflect.runtime.universe._
  import scala.tools.reflect.ToolBox
  val tb = currentMirror.mkToolBox()

  case class A(private val first: Int)

  case class B(first: Int, private val second: Int)

  def get(tpe: Type, position: Option[Int]): Tree = {
    val pos = s"${position.map(p => s"._${p + 1}").getOrElse("")}"
    tb.parse(s"(a: $tpe) => $tpe.unapply(a).get$pos")
  }

  println(tb.eval(get(typeOf[A], None)).asInstanceOf[(A) => Int](A(1)))

  println(tb.eval(get(typeOf[B], Some(1))).asInstanceOf[(B) => Int](B(1, 2)))

Also I added following dependencies:

scalaVersion := "2.11.8"
libraryDependencies ++= Seq(
  "org.scala-lang" % "scala-reflect" % scalaVersion.value,
  "org.scala-lang" % "scala-compiler" % scalaVersion.value
)

position is None when case class has only one parameter.

My solution is working, but how can I get rid of tb.parse(s"...") and replace it with quasiquote q"..."?

I tried that but it fails with:

Don't know how to unquote here
[error]     q"(a: $tpe) => $tpe.unapply(a).get$pos"
[error]                                        ^

As I understood I can not insert into quasiquote some string that's constructed at runtime and q"..." is parsed in compile time unlike tb.parse. Am I right?

Also is it safe to interpolate like that s"(a: $tpe) => $tpe.unapply(a).get$pos"? When using q"..." syntax quasiquote knows that $tpe is Type but string interpolation makes a string from it. I'm not sure that this will work always and in more complex and specific cases.


Solution

  • You are not right. Quasiquotes can't just interpolate normal strings, they must interpolate other pieces of AST. If you build up some fragments of AST yourself, then you can use them with q. Therefore, you can write this:

    def getElem(tpe: Type, pos: Option[Int]): Tree = {
      // Make something like TermName("_N")
      val tupleAccess = pos.map("_" + _).map(TermName.apply)
      val tupleExpr = {
        val plain = q"${tpe.typeSymbol.companion}.unapply(a).get"
        // Build into $companion.unapply(a).get(._N)
        tupleAccess.foldLeft(plain)(Select.apply)
      }
      q"(a: $tpe) => $tupleExpr"
    }
    

    And voilà!

    object A { object B { object C { case class D(private val x: Int, val y: String) } } }
    val all = tb.eval(getElem(typeOf[A.B.C.D], None)).asInstanceOf[A.B.C.D => (Int, String)]
    val int = tb.eval(getElem(typeOf[A.B.C.D], Some(1))).asInstanceOf[A.B.C.D => Int]
    val str = tb.eval(getElem(typeOf[A.B.C.D], Some(2))).asInstanceOf[A.B.C.D => String]
    val ds = {
      val D = A.B.C.D
      List(D(1, "one"), D(2, "two"), D(3, "three"))
    }
    ds.map(all) == List((1, "one"), (2, "two"), (3, "three"))
    ds.map(int) == List(1, 2, 3)
    ds.map(str) == List("one", "two", "three")