Scala - 使用带有两个通用参数的超类型如何导致 Scala 类型检查器以不同方式对待子类型?

Scala - how come using a super-type with two generic parameters cause the scala type checker to treat the child-type differently?

我运行陷入了一个有趣的境地。我想实现如下所示的东西。

object Test {
  abstract class Key[A]
  class Constraint[-A] {
    def doSomething(a: A): String = ""
  }

  object DesiredKeyConstraints {
    case class KeyConstraint[A](val key: Key[A], constraint: Constraint[A])
    val data: Map[Key[_], KeyConstraint[_]] = Map()
  }

  def useTheKeyConstraints[A](key: Key[A], value: A): String = {
    DesiredKeyConstraints.data.get(key).fold[String]("") {
      case DesiredKeyConstraints.KeyConstraint(_, constraint) =>  constraint.doSomething(value)
    }
  }

  def main(args: Array[String]) {
    println("hi")
  }
}

不幸的是,当我从映射中拉出一个 KeyConstraint 时,我不再知道它的类型。因此,当我尝试调用 doSomething 时,类型不会检出。这一切似乎都符合预期。有趣的是,在代码库的其他地方,我们有如下内容:(将 DesiredKeyConstraints 替换为 WorkingKeyConstraints

object Test {
  abstract class Key[A]
  class Constraint[-A] {
    def doSomething(a: A): String = ""
  }

  object WorkingKeyConstraints {
    sealed trait SuperTrait[A, B] {
      val key: Key[A]
    }
    case class KeyConstraint[A](val key: Key[A], constraint: Constraint[A]) extends SuperTrait[A, Unit]
    val data: Map[Key[_], SuperTrait[_, _]] = Map()
  }

  def useTheKeyConstraints[A](key: Key[A], value: A): String = {
    WorkingKeyConstraints.data.get(key).fold[String]("") {
      case WorkingKeyConstraints.KeyConstraint(_, constraint) =>  constraint.doSomething(value)
    }
  }

  def main(args: Array[String]) {
    println("hi")
  }
}

这个可以编译运行。出于某种原因,拥有超类型意味着当我们从 Map 中提取 KeyConstraint 时,它会将其视为 KeyConstraint[Any] 而不是 KeyConstraint[_]。因为 Constraint 是逆变的,我们可以将 Constraint[Any] 视为 Constraint[A],因此代码可以编译。这里的关键 problem/question 是,为什么拥有超类型会导致类型检查器将其视为 KeyConstraint[Any]?

此外,作为进一步的信息,我进一步研究了这个问题,它是具有两个泛型类型参数的超类型所特有的。如果我用两个泛型类型做 child-class 或者用一个泛型类型做父代,它仍然会失败。在下面查看我的其他失败尝试:

object AnotherCaseThatDoesntWorkKeyConstraints {
  case class KeyConstraint[A, B](val key: Key[A], constraint: Constraint[A])
  val data: Map[Key[_], KeyConstraint[_, _]] = Map()
}

object AThirdCaseThatDoesntWorkKeyConstraints {
  sealed trait SuperTrait[A] {
    val key: Key[A]
  }
  case class KeyConstraint[A](val key: Key[A], constraint: Constraint[A]) extends SuperTrait[A]
  val data: Map[Key[_], SuperTrait[_]] = Map()
}

我认为这是 Scala 类型检查器中的某种错误,但也许我遗漏了什么。

tl;dr 类型擦除和模式匹配

键入 MapSuperTrait 隐藏了有关类型的信息,并导致模式匹配为您的提取器假设一个广泛的类型。


这是一个类似的示例,但使用 Any 而不是您的 SuperTrait。此示例还展示了如何从中生成运行时异常。

case class Identity[A : Manifest]() {
  def apply(a: A) = a match { case a: A => a } // seemingly safe no-op
}

val myIdentity: Any = Identity[Int]()

myIdentity match {
  case f@Identity() => f("string") // uh-oh, passed String instead of Int
}

抛出异常

scala.MatchError: string (of class java.lang.String)
  at Identity.apply(...)

f@Identity() 模式匹配 Any 作为 Identity[Any],并且由于类型擦除,这匹配 Identity[Int],这变成了错误。


相反,如果我们将 Any 更改为 Identity[_]

case class Identity[A : Manifest]() {
  def apply(a: A) = a match { case a: A => a }
}

val myIdentity: Identity[_] = Identity[Int]()

myIdentity match {
  case f@Identity() => f("string")
}

正确地编译失败​​。

found   : String("string")
required: _ where type _
     case f@Identity() => f("string")

知道f是存在类型Identity[T] forSome {type T},不能证明String符合通配类型T.


在第一个示例中,您有效地进行了模式匹配

DesiredKeyConstraints.KeyConstraint[Any](_, constraint)

第二个信息比较多,你匹配的是

DesiredKeyConstraints.KeyConstraint[T](_, constraint) forSome {type T}

(这只是说明性的;您目前无法在模式匹配时实际编写类型参数。)