如何在Scala宏中模式匹配类型?

时间:2018-01-22 20:52:34

标签: scala macros scala-macros

我的项目中有一个方法,包含宏(whitebox),尝试从MethodSymbol的返回类型验证并提取类型参数。
这是代码(放在一些带有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, ?]]")
    }


    ???
  }
}

在编译期间,它说Warning:scalac: scala.concurrent.Future[Either[String, Int]]是正确的,但是在它之后它会在c.abort调用停止,这意味着模式与类型不匹配。

我甚至尝试在REPL中调试它,但这是我得到的:

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

我已经尝试了很多次但最终总是将这些类型作为String处理,这是非常不清楚的。
谢谢你的回复!

1 个答案:

答案 0 :(得分:1)

如果有人展示了使用quasiquotes和模式匹配来解构Type的例子,我将不胜感激。目前在我看来,Type和quasiquotes是不同宇宙的一部分(不在Scala内部意义上)并且不能相互作用。我知道做这样的事情的最好方法就是这样的代码:

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, ?]]"))

我使用SomeType包装到Option中,并使用for-comprehension语法,恕我直言,让您更容易理解正在发生的事情,比您尝试过的更容易在Type上使用常规(基于非基于quququotes的)模式匹配。

更新:tq""哪里有效?

根据我的经验,您可以使用tq""解构类型的唯一上下文位于Macro annotations,可用于注释整个类或方法定义。请考虑以下示例:

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"))

}

我们希望CheckReturnTypeMacroAnnotation确保返回类型的格式为scala.concurrent.Future[scala.util.Either[String, ?]]。我们可以将CheckReturnTypeMacroAnnotation实施为

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
  }
}

但请注意,您必须如何处理具有不同但相似tq""模式的各种情况,并且未明确指定返回类型的fooBad无论如何都将失败。尝试使用此宏编译Test的输出将生成如下输出:

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

注意输出中实际存在的所有4个案例以及fooBad是否失败。这个问题似乎来自于宏在wavepechecker之前运行的事实。不幸的是,我不知道如何使这种模式匹配真正起作用。我调查了reifyc.typecheck,但没有运气。