为什么协变/逆变意味着只读/只写?

6
如果你查看接口中协变/逆变字段的流文档,协变意味着只读,而逆变意味着只写。但是,我不太明白为什么会这样。在他们的变异文档中,它们被定义为:
协变
- 协变不接受超类型。 - 协变接受子类型。
逆变
- 逆变接受超类型。 - 逆变不接受子类型。
但这并不真正映射到我的脑海中的只读/只写。有人能更深入地解释一下为什么会这样吗?

6
这与Haskell或函数式编程有什么关系? - dfeuer
1
@dfeuer 协变和逆变来自范畴论,因此我认为了解Haskell或FP的人比我只是标记为“flowtype”的人更有资格回答这个问题,后者只有不到1k的关注者。同样地,了解Haskell的人通常对类型理论有更深入的了解。 - m0meni
9
Haskell 没有普及的子类型,这就是泛型面向对象语言中协变性如此重要的原因,因为子类型在那里是普遍存在的。此外,我不知道它与范畴论有任何实质性的联系。"协变"和"逆变"这些词在范畴论之前已经存在很长一段时间了,它们的意思只是"随着/以相同方式变化/遵循"和"反对/以相反方式变化/分歧"。在协变和逆变函子中有联系,但需要一些工作来进行翻译。 - HTNW
3
我认为在这个上下文中,Contravariance和Covariance的意思是指https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)。在Haskell中,它们的意思完全不同(https://www.fpcomplete.com/blog/2016/11/covariance-contravariance)。 - Yuan Wang
5个回答

5

我不熟悉这种语言的语法,因此这个答案是伪代码。

假设我们有三种类型:暹罗猫 < 猫 < 动物,并定义一个接口。

interface CatCage {
    cat: Cat
}

编写一些方法

get_cat_in_cage (CatCage c) -> Cat {
    c.cat
}

put_cat_in_cage (Cat c, CatCage cage) {
    cage.cat = c
}

协变性

如果我们将这个领域变成协变的,我们可以定义一个实例,例如:

SiameseCage < CatCage {
    cat : Siamese
}

但如果我们这样做

put_cat_in_cage (aCat, aSiameseCage)

在这种情况下,aSiameseCage.cat的值是什么?SiameseCage认为它应该是一个Siamese,但我们只能使它成为一个Cat - 显然,该字段不能在接口上可写并且同时具有协变性。
逆变性
如果我们将该字段定义为逆变,则可以定义一个实例,例如:
AnimalCage < CatCage {
    cat : Animal
}

但现在我们无法做到

get_cat_in_cage (anAnimalCage)

由于无法保证anAnimalCage.cat的值是一个Cat,所以如果它是协变的,该字段就不能在接口上可读。

您可以通过返回一个Object或基本类型来使其在接口上可读,但这可能没有任何实际用例,因此语言在决定不这样做时是明智的。


4
您最常遇到差异的地方是函数参数和返回值。函数在其参数方面是反变的,在其返回值方面是协变的。
从只读和只写变量的对称性角度考虑函数是一种获取直觉的方法,因为从调用它的代码的角度来看,参数是只写的:您将参数传递给函数,但在该函数之外的任何代码都无法找出您传递了什么或函数将其视为何种类型内部。同样,返回值是只读的:当您调用函数时,它会给您一些东西,您无法将其放回。而它给您的值可以是您预期的任何子类型。
任何只读数据都是协变的,因为它允许给你更多你要求的东西(一个子类型)。作为只读数据的用户,您仅使用期望类型的功能,并忽略实际上得到的子类型的额外功能。
任何只写数据都是反变的,因为即使您提供了它所要求的确切类型,它也可以选择不使用所有功能,并将您提供的内容视为超类型。例如,错误记录器可以接受具有日期、错误代码等复杂错误对象,但实际上委派给仅记录消息的简单记录器。

3

既然你标记了这个, 我就可以放心地使用一些Glasgow扩展的Haskell了。

{-# language GADTs, ConstraintKinds
  , TypeOperators, ScopedTypeVariables, RankNTypes #-}

import Data.Constraint
import Data.Kind

data Foo :: (Type -> Constraint) -> Type where
  Foo :: forall a. c a => a -> Foo c

upcast :: forall c d. (forall a. c a :- d a) -> Foo c -> Foo d
upcast cd (Foo (a :: a))
  | Sub Dict <- cd :: c a :- d a
  = Foo a

假设我有一个 IORef (Foo c)。我可以轻松地从中 读取 一个 Foo d
readDFromC :: (forall a. c a :- d a) -> IORef (Foo c) -> IO (Foo d)
readDFromC cd ref = upcast cd <$> readIORef ref

同样地,我可以进行双重翻转,将 Foo d 替换为 Foo c

writeCToD :: (forall a. c a :- d a) -> (Foo d -> Foo c) -> IORef (Foo d) -> IO ()
writeCToD cd f ref = modifyIORef ref (upcast cd . f)

但是,如果你尝试单独翻转,你会陷入困境,因为无法从d推导出c


1
我本来会说自己“懂Haskell”,但我发现这很难读懂;我想,那些寻找一般原理而非特定于Haskell的人将会很难跟进这个论点。 - Cubic
@Cubic,说实话,我当时有点傻乎乎的心情。 - dfeuer
@Cubic,使用量化约束可能会使得这部分内容更加友好(或者不那么糟糕),但这仍然是相当前沿的技术。 - dfeuer

3
逆变性只是意味着“方向相反”(而协变性则只是指“方向相同”)。在子类型关系的上下文中,如果一个复合类型是另一个类型的子类型,当且仅当它的某个部分是另一个类型中相同部分的超类型时,就称为逆变性。
“复合类型”只是指具有其他组件类型的类型。像Haskell、Scala和Java这样的语言通过声明一个类型具有参数(Java称之为“泛型”)来处理这个问题。从对Flow文档链接的简要查看中,看起来Flow没有正式定义参数,并且实际上认为每个属性的类型都是单独的参数。因此,我将避免具体细节,只谈论由其他类型组成的类型。
子类型化都是关于可替换性的。如果有人想要一个T,我可以给他们任何T的子类型的值,什么也不会出错;他们能够使用所请求的内容进行的“允许”操作仅限于可能适用于任何可能的T的操作。当类型具有其他类型的子结构时,则会出现逆变性。如果有人请求一个包含组件类型T的结构类型,并且我想给他们一个具有相同结构但组件类型是S的值,那么什么时候合法呢?
如果组件类型存在是因为他们可以使用所请求的对象获得T值(例如读取属性或调用返回T值的方法),则当我给他们我的值时,他们将从中获取S值而不是他们期望的T值。他们想要对这些值进行Tish操作,这只有在ST的子类型时才能正常工作。因此,对于复合类型,我必须是所需类型的子类型,我具有的部分必须是所需类型中的部分的子类型。这就是协变性。
另一方面,如果组件类型存在是因为它们可以向它们请求的对象发送T值(例如编写属性或调用以T值作为参数的方法),那么当我给它们我的值时,它们将期望将其发送S值而不是T值。我的对象将希望使用其他人将要发送给它的T值进行Sish操作。这只有在T是S的子类型时才能正常工作。因此,在这种情况下,对于我必须是所需类型的子类型的复合类型,我拥有的一个组件必须是所需类型中组件的超类型。这就是逆变性。
简单函数类型是一个具体的例子,通过一点思考通常很容易理解。在Haskell符号表示法中编写的函数类型类似于ArgumentType -> ResultType; 这本身就是一个具有两个组件类型的复合类型,因此我们可以询问一个函数类型是否可以替换另一个函数类型(是子类型)。
假设我有一些Dog值的列表,并且我需要映射一个函数来将其转换为Cat值的列表。因此,执行映射的函数希望我提供一个类型为Dog-> Cat的函数。
我可以提供类型为GreyHound-> Cat的函数吗?不可以。映射函数将在列表中的所有Dog值上调用我的函数,而我们不知道它们是否全部是GreyHound值。
我可以提供类型为Mammal-> Cat的函数吗?可以。我的函数只能执行任何哺乳动物都有效的操作,这显然包括它将被调用的列表中的所有Dog值。
我可以提供类型为Dog-> Siamese的函数吗?可以。映射函数将使用此函数返回的Siamese值来构建Cat列表,并且Siamese值是Cat值。
我可以提供类型为Dog-> Mammal的函数吗?不可以。此函数可能会将Dog转换为鲸鱼,而鲸鱼无法适应映射函数需要构建的Cat列表。

0
考虑基本类型Animal和子类型DogCat
编写视为喂养,将阅读视为查看毛皮
Animal animal = new Cat();
AnimalFood food = new DogFood();

AnimalFur fur = animal.GetFur(); // ALLOWED: Both dog fur and cat fur are always animal fur.
animal.Feed(food); // NOT ALLOWED: There are some dog foods cats can't eat.

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