问题描述
考虑协变类型参数 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 的想法,我将提供直观的解释,而不是正式的解释。既是因为我不知道正式的细节,也是因为我希望这对读者更有帮助。
首先是一些免责声明:
- 我可能有错别字或一些错误,如果您注意到,请编辑答案。
- 如上所述,我要描述的心智模型是一个近似值,在编译时和运行时实际发生的情况会有所不同。
- 在此回答中,我将提到可互换的类型和类。这是错误的,我自己已经在其他答案中指出了这一点。在这种情况下,是为了简化这种心理模型;但我建议在点击差异后,将类型和类别的区别混合到该模型中:https://typelevel.org/blog/2017/02/13/more-types-than-classes.html
- 结合上一点,我将在我的示例中使用具体/简单类型。幸运的是,由于类型擦除和参数化,我将解释的内容适用于类型构造函数和其他复杂类型。
- 我还将交替使用方法和函数,这也是错误: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
的 baz
和 Foo
方法会发生什么
由于 Foo[Dog] <: Foo[Animal]
我可以将前者转换为后者,那么给定 Cat <: Animal
我也可以将前者转换为后者。
最后,我可以将伪装为 Cat
的 Animal
传递给伪装为 bar
的 Foo[Dog]
的 Foo[Animal]
方法,但是在运行时我们将传递一个Cat
到需要 Dog
kataplum 的方法!当然,除非这种方法总是为这种情况做好准备。
这就是为什么 bar
必须像 [B >: A](b: B)
一样定义。这里我们说我们可以接受任何 B
编译器可以为其生成隐式强制转换函数 A => B
(与以前相反,感谢 Any
这样的类型和这样的函数总是可能的)。然后 bar
的实现应该能够适用于那个新类型 B
并在需要时使用 cast 函数。
这意味着前面的示例不会在运行时爆炸,因为我们可以直接传递 Cat
而无需通过间接伪装;这是有效的,因为编译器总是能够推断出 B
应该是 Animal
(Cat
和 Dog
的 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 节中)。