jenda
jenda

Reputation: 346

How to pattern match types in Scala macros?

I have a method in my project with macros (whitebox), trying to verify and extract type arguments from a return type of a MethodSymbol.
Here is the code (placed inside some class with import c.universe._):

private object ImplMethod {
  def apply(m: MethodSymbol): ImplMethod = {

    println(m.returnType)

    val respType = m.returnType match {
      case tq"scala.concurrent.Future[scala.util.Either[String, ${resp}]]" => resp
      case _ =>
        c.abort(c.enclosingPosition, s"Method ${m.name} in type ${m.owner} does not have required result type Future[Either[String, ?]]")
    }


    ???
  }
}

During the compilation it says Warning:scalac: scala.concurrent.Future[Either[String, Int]] which is right, however right after that it stops on the c.abort call which means the pattern doesn't match the type.

I even tried to debug it in REPL, but here is what I got:

val tq"scala.concurrent.Future[$a]" = typeOf[scala.concurrent.Future[Int]]

scala.MatchError: scala.concurrent.Future[Int] (of class 
scala.reflect.internal.Types$ClassArgsTypeRef)
  ... 28 elided

I tried this many times already but always ended up with handling those types as Strings which is very unclear.
Thanks for reply!

Upvotes: 0

Views: 857

Answers (1)

SergGr
SergGr

Reputation: 23788

I'd be grateful if someone showed an example of deconstructing Type using quasiquotes and pattern-matching. For now it seems to me that Type and quasiquotes are parts of different universes (not in Scala internals sense) and can't interact. The best way I know to do something like this is code like this:

val string = typeOf[String].dealias
val future = typeOf[scala.concurrent.Future[_]].typeConstructor
val either = typeOf[scala.util.Either[_, _]].typeConstructor

val respType = (for {
  f <- Some(m.returnType.dealias) if f.typeConstructor == future // Future[_]
  e <- f.typeArgs.headOption if e.typeConstructor == either // Future[Either[_,_]]
  ea <- Some(e.typeArgs) if ea.head.dealias == string // Future[Either[String,_]]
} yield ea(1))
  .getOrElse(c.abort(c.enclosingPosition, s"Method ${m.name} in type ${m.owner} does not have required result type Future[Either[String, ?]]"))

I use Some to wrap Type into Option and use for-comprehension syntax that IMHO makes it easier to understand what's going on, much easier than what you could get if you tried to use usual (non-quasiquotes-based) pattern matching on Type.


Update: Where tq"" works at all?`

From my experience the only context in which you can use tq"" to deconstruct type is in Macro annotations that can be used to annotate whole classes or method definitions. Consider following example:

import scala.concurrent.Future
import scala.util.Either

class Test {

  @CheckReturnTypeMacroAnnotation
  def foo1(): scala.concurrent.Future[scala.util.Either[String, Short]] = ???

  @CheckReturnTypeMacroAnnotation
  def foo2(): Future[Either[String, Int]] = ???

  @CheckReturnTypeMacroAnnotation
  def foo3(): scala.concurrent.Future[Either[String, Long]] = ???

  @CheckReturnTypeMacroAnnotation
  def foo4(): Future[scala.util.Either[String, Double]] = ???

  @CheckReturnTypeMacroAnnotation
  def fooBad()  = scala.concurrent.Future.failed[scala.util.Either[String, Short]](new RuntimeException("Fake"))

}

We want CheckReturnTypeMacroAnnotation to ensure that the return type is of form scala.concurrent.Future[scala.util.Either[String, ?]]. We may implement CheckReturnTypeMacroAnnotation as

import scala.language.experimental.macros
import scala.annotation.{StaticAnnotation, compileTimeOnly}


@compileTimeOnly("enable macro to expand macro annotations")
class CheckReturnTypeMacroAnnotation extends StaticAnnotation {
  def macroTransform(annottees: Any*) = macro CheckReturnTypeMacro.process
}


object CheckReturnTypeMacro {

  import scala.reflect.macros._
  import scala.reflect.macros.whitebox.Context

  def process(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._
    val methodDef = annottees.map(_.tree).toList match {
      case x :: Nil => x
      case _ => c.abort(c.enclosingPosition, "Method definition is expected")
    }
    //c.warning(c.enclosingPosition, s"methodDef ${methodDef.getClass} => $methodDef")

    val returnType = methodDef match {
      case q"$mods def $name[..$tparams](...$paramss): $tpt = $body" => tpt
      case _ => c.abort(c.enclosingPosition, "Method definition is expected")
    }
    //c.warning(NoPosition, s"returnType ${returnType.getClass} => $returnType")

    val respType = returnType match {
      case tq"scala.concurrent.Future[scala.util.Either[String, ${resp}]]" =>
        c.warning(c.enclosingPosition, s"1 resp ${resp.getClass} => $resp")
        resp
      case tq"Future[Either[String, ${resp}]]" =>
        c.warning(c.enclosingPosition, s"2 resp ${resp.getClass} => $resp")
        resp
      case tq"scala.concurrent.Future[Either[String, ${resp}]]" =>
        c.warning(c.enclosingPosition, s"3 resp ${resp.getClass} => $resp")
        resp
      case tq"Future[scala.util.Either[String, ${resp}]]" =>
        c.warning(c.enclosingPosition, s"4 resp ${resp.getClass} => $resp")
        resp

      case _ =>
        c.abort(c.enclosingPosition, s"Method does not have required result type Future[Either[String, ?]]")
    }

    c.Expr[Any](methodDef) //this is in fact a no-op macro. it only does verification of return type
  }
}

Note however how you have to handle various cases with different but similar tq"" patterns and that fooBad that does not explicitly specify return type will fail anyway. The output of an attempt to compile Test with this macro will generate an output like this:

Warning:(18, 8) 1 resp class scala.reflect.internal.Trees$Ident => Short
      @CheckReturnTypeMacroAnnotation
Warning:(21, 8) 2 resp class scala.reflect.internal.Trees$Ident => Int
      @CheckReturnTypeMacroAnnotation
Warning:(24, 8) 3 resp class scala.reflect.internal.Trees$Ident => Long
      @CheckReturnTypeMacroAnnotation
Warning:(27, 8) 4 resp class scala.reflect.internal.Trees$Ident => Double
      @CheckReturnTypeMacroAnnotation
Error:(31, 8) Method does not have required result type Future[Either[String, ?]]
      @CheckReturnTypeMacroAnnotation

Note how all 4 cases actually present in the output and that fooBad has failed. The issue seems to come from the fact that macro is run before typechecker. Unfortunately I'm not aware of a way to make this pattern matching really work. I looked into reify and c.typecheck but had no luck.

Upvotes: 2

Related Questions