我们可以优雅地匹配scala中的擦除类型吗?

时间:2016-02-03 15:49:25

标签: scala

是否有任何优雅的方式,来自:

def foo[T: TypeTag](a: A[T]) {

  // can we match on the type of T here?

}

在T类型的匹配表达式上?

显然下面的内容并没有克服T的删除,所以我们必须手工检查TypeTag吗?

  a match {
    case _:A[SomeSpecificType] => ...

或者scala能为此提供一些优雅吗?

1 个答案:

答案 0 :(得分:4)

可悲的是,因为如果向模式添加类型检查,编译器不会考虑类型标记。我不确定为什么以及这是否有计划。但是,您可以比较类型标记的相等性:

typeOf[T] =:= typeOf[List[String]]

您可以在if或match条件中使用它,然后转换为目标类型。

在思考了一下之后,我意识到编写自己的模式提取器很容易,它隐藏了检查和转换:

import scala.reflect.runtime.universe._
class TypeTest[A: TypeTag]() {
  def unapply[B: TypeTag](v: B): Option[A] =
    if(typeOf[B] <:< typeOf[A])
      Some(v.asInstanceOf[A])
    else
      None
}
object TypeTest {
  def apply[A: TypeTag] = new TypeTest()
}

现在,我们可以做这样的事情:

def printIfStrings[T: TypeTag](v: T) {
  val string = TypeTest[List[String]]
  v match {
    case string(s) => printString(s)
    case _ =>
  }
}

def printString(s: List[String]) {
  println(s)
}

printIfStrings(List(123))
printIfStrings(List("asd"))

这已经非常简洁,但由于Scala不支持将参数直接传递给模式中的提取器,我们必须在匹配表达式之前将所有提取器定义为val string

宏可以转换代码,因此将匹配表达式中任何未经检查的类型检查转换为适当的模式或直接使用类型标记添加显式检查应该很容易。

然而,这要求我们在每个关键匹配表达式周围都有一个宏调用,这将非常难看。另一种方法是通过一些将部分函数作为参数的方法调用来替换匹配表达式。可以使用隐式转换为任意类型提供此方法。

然后唯一剩下的问题是编译器在调用任何宏之前对代码进行类型检查,因此即使现在已经检查,它也会为未经检查的强制转换生成警告。我们仍然可以@unchecked来压制这些警告。

我选择用上面描述的提取器替换模式中的类型检查,而不是向case添加条件和显式类型转换。原因是这个转换是局部的(我只需要用另一个替换子表达式)。

所以这是宏:

import scala.language.experimental.macros
import scala.language.implicitConversions
import scala.reflect.macros.blackbox.Context

object Switch {

  implicit class Conversion[A](val value: A) {
    def switch[B](f: PartialFunction[A, B]): B = macro switchImpl
  }

  def switchImpl(c: Context)(f: c.Tree): c.Tree = {
    import c.universe._

    val types = collection.mutable.Map[Tree,String]()
    val t1 = new Transformer {
      override def transformCaseDefs(trees: List[CaseDef]) = {
        val t2 = new Transformer {
          override def transform(tree: Tree) = {
            def pattern(v: String, t: Tree) = {
              val check = types.getOrElseUpdate(t, c.freshName())
              pq"${TermName(check)}(${TermName(v)})"
            }
            tree match {
              case Bind(TermName(v),Typed(Ident(termNames.WILDCARD),
                  Annotated(Apply(
                    Select(New(Ident(TypeName("unchecked"))),
                    termNames.CONSTRUCTOR), List()
                  ), t)))
                => pattern(v,t)
              case Bind(TermName(v),Typed(Ident(termNames.WILDCARD),t)) 
                => pattern(v,t)
              case _ => super.transform(tree)
            }
          }
        }
        t2.transformCaseDefs(trees)
      }
    }
    val tree = t1.transform(c.untypecheck(f))
    val checks =
      for ((t,n) <- types.toList) yield 
        q"val ${TermName(n)} = Switch.TypeTest[$t]"

    q"""
      ..$checks
      $tree(${c.prefix}.value)
    """
  }

  import scala.reflect.runtime.universe._
  class TypeTest[A: TypeTag]() {
    def unapply[B: TypeTag](v: B): Option[A] =
      if(typeOf[B] <:< typeOf[A]) Some(v.asInstanceOf[A])
      else None
  }
  object TypeTest {
    def apply[A: TypeTag] = new TypeTest()
  }
}

现在神奇地对模式中的类型检查起作用:

import Switch.Conversion
val l = List("qwe")

def printIfStrings2[T: scala.reflect.runtime.universe.TypeTag](v: T) {
  v switch {
    case s: Int => println("int")
    case s: List[String] @unchecked => printString(s)
    case _ => println("none")
  }
}

printIfStrings2(l)
printIfStrings2(List(1, 2, 3))
printIfStrings2(1)

我不确定我是否正确处理了所有可能的情况,但我尝试过的每件事情都很好。如果@unchecked也注释了具有多个注释的类型,则可能无法正确处理,但我无法在标准库中找到一个示例来测试它。

如果省略@unchecked,结果完全相同,但如上所述,您将收到编译器警告。我没有看到用普通宏去掉那个警告的方法。也许注释宏可以做到,但它们不在Scala的标准分支中。