题外:事实证明,每次我心血来潮都没好结果。
假设A是B的子类,那么List[A]是List[B]的子类吗?A -> C是B -> C子类吗?协变和逆变讨论的就是高阶类型和子类型组合时的子类关系。
A是B的子类,意味着需要父类B的地方,可以替换成子类A,这在面向对象编程里是很自然的,最明显的表现就是可以把A的实例赋值给一个B变量。
下面用A <: B
表示A是B的子类。假设 A <: B,给定一个类型构造子T,则T是:
- 协变(covariance):T[A] <: T[B]
- 逆变(contravariance):T[B] <: T[A] (这里A和B的顺序反了,所以是“逆”)
- 不变(invariance):T[A]和T[B]没有子类关系
函数
问题1:假设 A <: B,(A -> C) 和 (B -> C) 的子类关系是?
问题2:假设 A <: B,(C -> A) 和 (C -> B) 的子类关系是?
只要假设一个应用场景,根据LSP原则进行推导,就能知道函数类型:
- 对结果是协变的,或者说结果在函数类型的协变位置或正位置。
- 对参数是逆变的,或者说参数在函数类型的逆变位置或负位置。
问题9:什么情况下,((A -> C) -> C) <: ((B -> C) -> C)?
我们也可以假设一个使用场景,然后进行推导,但是这有点复杂了。有一个简单的规则可以推导出每个位置是逆变还是协变位置:
一个位置在整个项中的正负是它所在的每个子项的正负的乘积
好绕,不过看一下例子就能明白,以(A -> B) -> C为例,求A的所在位置的正负:
- 在(A -> B)中,A在负位置
- 在(A -> B) -> C中,(A -> B) 在负位置
- 所以A在(A -> B) -> C中的位置是:负 * 负 = 正
回到问题9,((A -> C) -> C) <: ((B -> C) -> C),A、B又在协变位置(正位置),所以 A <: B。
问题4:什么情况下,((A1 -> B1) -> C1) -> (D1 -> E1)) <: ((A2 -> B2) -> C2) -> (D2 -> E2))
答案和出处都见参考2。
为什么这个规则有效呢?我尝试从实际参数的持有者来理解,没有结果,然后看了点范畴论。
范畴论
协变和逆变来自范畴论里的协变和逆变函子(Functor)。在我们讨论的这个范畴里,对象是类型,态射是子类关系。
例1. 下面这个范畴里有类型A和B,箭头由子类指向父类:
类型构造子是Functor,把一个范畴映射到另一个范畴。Covariant functor会保留原范畴间的箭头方向,contravariant functor会把箭头反转。
例2. T是covariant functor,G是contravariant functor,T[A]和G[A]是新范畴里的对象,新范畴里对像也是类型,态射也是子类关系,所以T和G还是自函子(红蓝箭头都是由子类指向父类,红箭头用来突出箭头方向和最初的反了,黑箭头是functor的映射)。
现在考虑函数类型的构造子->
,它有两个参数,范畴的积不太好理解,但是我们可以部分调用得到一个functor。-> A
表示结果为A的函数,是逆变的,A ->
表示参数为A的函数,是协变的。
现在看((A -> B) -> C)->D中的B:
也就是说,经过一系列的逆变和协变,B在((A -> B) -> C)->D的协变位置,和正负乘积的关系就很明显了。
什么用?
还是那句话,用类型更准确地描述需求。