在函数参数 A => B 的逆变位置接受协变类型 A

问题描述

考虑协变类型参数 A

case class Foo[+A](a: A):
  def bar(a: A) = a             // error: covariant type A occurs in contravariant position 
  def zar(f: A => Int) = f(a)   // ok
             |
          This is contravariant position. Why is it ok?

Foo(41).zar(_ + 1) // : Int = 42

当它出现在 zar 的逆变位置时,为什么它被接受为 A => Int 的参数?

解决方法

根据@sarveshseri 的想法,我将提供直观的解释,而不是正式的解释。既是因为我不知道正式的细节,也是因为我希望这对读者更有帮助。

首先是一些免责声明:

  1. 我可能有错别字或一些错误,如果您注意到,请编辑答案。
  2. 如上所述,我要描述的心智模型是一个近似值,在编译时和运行时实际发生的情况会有所不同。
  3. 在此回答中,我将提到可互换的类型和类。这是错误的,我自己已经在其他答案中指出了这一点。在这种情况下,是为了简化这种心理模型;但我建议在点击差异后,将类型和类别的区别混合到该模型中:https://typelevel.org/blog/2017/02/13/more-types-than-classes.html
  4. 结合上一点,我将在我的示例中使用具体/简单类型。幸运的是,由于类型擦除和参数化,我将解释的内容适用于类型构造函数和其他复杂类型。
  5. 我还将交替使用方法和函数,这也是错误https://docs.scala-lang.org/tutorials/FAQ/index.html#whats-the-difference-between-methods-and-functions

现在让我们开始吧。首先让我们假设,如果一个方法接受 Foo,那么它只能接受 Foo 类型的值,而不能接受其他类型的值。

然后让我们假设子类型实际上是一个 “隐式” 函数,它“转换” 值。所以 B <: A 就是 B => A
让我们想象一下,这样的“cast”是一种伪装,价值实际上是相同的,只是看到的不同(这基本上是Liskov原则).

因此,当您尝试将 B 传递给需要 A 的方法时,编译器将插入此隐式转换。这样在运行时,该方法接收的值看起来像是 A 类型之一而不是 B 类型之一;但该值仍然属于 B (实际上我们在这里讨论的是而不是类型,但我希望你能明白).

然后让我们看看协变类 bar
bazFoo 方法会发生什么 由于 Foo[Dog] <: Foo[Animal] 我可以将前者转换为后者,那么给定 Cat <: Animal 我也可以将前者转换为后者。 最后,我可以将伪装为 CatAnimal 传递给伪装为 barFoo[Dog]Foo[Animal] 方法,但是在运行时我们将传递一个Cat 到需要 Dog kataplum 的方法!当然,除非这种方法总是为这种情况做好准备。

这就是为什么 bar 必须像 [B >: A](b: B) 一样定义。这里我们说我们可以接受任何 B 编译器可以为其生成隐式强制转换函数 A => B (与以前相反,感谢 Any 这样的类型和这样的函数总是可能的)。然后 bar 的实现应该能够适用于那个新类型 B 并在需要时使用 cast 函数。
这意味着前面的示例不会在运行时爆炸,因为我们可以直接传递 Cat 而无需通过间接伪装;这是有效的,因为编译器总是能够推断出 B 应该是 Animal CatDog 的 LUB) 所以它会强制转换Cat 作为 Animal 并将其传递给 bar 以及强制转换函数 Dog => Animal

注意,A => B 强制转换函数的存在意味着编译器也可以创建 F[A] => F[B] 函数,如果 F 是协变的。

现在让我们看看 baz 会发生什么。 同样,我们将 Foo[Dog] 转换为 Foo[Animal],然后我们将尝试使用函数 baz 调用 Animal => Int,该函数应该在运行时工作,因为我们甚至不需要伪装一下,我们可以将这样的函数直接传递给 Foo[Dog] 因为 (Animal => Int) <: (Dog => Int) 这是因为函数的输入是逆变的。

但是这到底是怎么工作的呢?很简单,直觉告诉我们,如果我能够处理/消费/接收/使用任何 Animal,那么我应该能够处理任何 Dog,因为它们是 Animals,对吗?让我们看看它如何与我们的心智模型一起工作。

我有 baz(Dog => Int) 并且我有 f(Animal => Int) 编译器可以做的是创建一个新函数 g(Dog => Int) = cast(Dog => Animal) andThen f(Animal => Int) 并改用 g

希望这会有所帮助,请随时留下任何问题。

,

您的班级可以被视为

trait Function1[-Input,+Result]  // Renamed the type parameters for clarity

case class Foo[+A](a: A) {
  val bar: Function1[A,A] = identity
  val zar: Function1[Function1[A,Int],Int] = { f => f(a) }
}

编译器采取的方法是在签名中的每个类型位置分配正面、负面和中性注释;我将通过将 + 和 - 置于该位置的类型之后来标记位置

首先顶级 val 是正的(即协变):

case class Foo[+A](a: A) {
  val bar: Function1[A,A]+
  val zar: Function1[Function1[A,Int]+
}

bar 可以是 Function1[A,A] 的任何子类型,zar 可以是 Function1[Function1[A,Int] 的任何子类型,所以这是有道理的(LSP 等)。

然后编译器进入类型参数。

case class Foo[+A](a: A) {
  val bar: Function1[A-,A+]
  val zar: Function1[Function1[A,Int]-,Int+]
}

由于 Input 是逆变的,这将相对于其周围的分类“翻转”分类(+ -> -,- -> +,中性不变)。 Result 协变不会翻转分类(如果 Function1 中有一个不变参数,这将强制分类为中性)。第二次应用这个

case class Foo[+A](a: A) {
  val bar: Function1[A-,A+]
  val zar: Function1[Function1[A+,Int-],Int+]
}

定义的类的类型参数只能用在 + 位置,如果它是协变的,-位置如果是逆变的,如果它是不变的,则可以在任何地方使用(Int,它不是类型参数,可以被认为是不变的此分析的目的:即,一旦我们获得既不是类型参数也没有类型参数的类型,我们就可以免除注释)。在 bar 中,我们有冲突(A-),但在 zar 中,A+ 表示没有冲突。

Luis 的回答中呈现的生产/消费关系是一个很好的直观总结。这是对编译器如何实际得出 A 中的 zar 是处于协变位置; Scala 编程(Odersky、Spoon、Venners)更详细地描述了它(在第 3 版中,它在第 19.4 节中)。