在Scala中,我们能否优雅地匹配已擦除的类型?

3

有没有一种优雅的方式可以从以下位置到达:

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

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

}

如何针对T的类型进行匹配表达式?

显然,下面的代码并没有克服擦除的问题,因此我们必须手动检查TypeTag吗?

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

或者说Scala能为此提供一些优雅的解决方案吗?


这个问题有一个开放的增强票据,网址是https://issues.scala-lang.org/browse/SI-6517。 - Seth Tisue
@SethTisue 谢谢您提到了那个闲置问题… 如果我必须为下一个 Scala 版本选择一项改进,那就是简化类型系统以实现类型擦除的弹性。 根据我的经验,任何严格的类型架构都很快会遇到这个玻璃封闭问题,导致非常粗略的样板文件和不成体系的代码。 到最后,一个带有类型擦除的类型系统其实是自相矛盾的… 这能被“修复”吗?我不知道。 - matanster
1个回答

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字符串

宏可以转换代码,因此很容易将匹配表达式中的任何未检查的类型检查转换为适当的模式或使用类型标签直接添加显式检查。
然而,这需要我们在关键的匹配表达式周围包装一个宏调用,这将非常丑陋。另一种选择是将匹配表达式替换为带有部分函数作为参数的某个方法调用。这个方法可以使用隐式转换为任意类型提供。
唯一剩下的问题是编译器在调用任何宏之前对代码进行类型检查,所以即使现在已经检查过了,它仍会为未检查的转换生成警告。我们仍然可以使用@unchecked来抑制这些警告。
我选择使用上面描述的提取器替换模式中的类型检查,而不是在案例中添加条件和显式类型转换。原因是这个转换是局部的(我只需要用另一个子表达式替换一个子表达式)。
这就是宏:
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的标准分支中。

感谢您提供的精准答案。我会保持这个问题开放,以防出现其他情况。 - matanster
好的,我又弹出来了 ;-) 这个解决方案实际上比直接使用 =:= 要好得多,不幸的是无法直接给提取器传递参数,所以这可能是最好的了。 - dth
感谢这个精彩的点睛之笔,我也分享大家的悲伤,事实上很遗憾我们使用一种强调精细的类型系统的语言……却运行在一个会抹掉其类型信息的运行时环境中。 - matanster
宏可能会进一步简化事情 :) - matanster
宏无法真正修改语法。似乎有一个编译器插件可以允许传递参数到提取器,这将是一种改进。此外,可以编写一个宏,称为“ematch”,它以部分函数作为参数,因此看起来像匹配语句。这可以用于实现类型检查。然而,在应用宏之前,编译器可能仍会生成警告,并且为部分函数生成的代码非常复杂。将其转换为所需的形式可能很困难,但是有可能。如果我感到无聊,我可能会尝试做到这一点;) - dth
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接