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

问题描述 投票:3回答:4

如果你看一下接口中的the flow docs on covariant/contravariant字段,covariant意味着只读,而逆变则意味着只写。但是,我真的不明白为什么。 In their docs on variance,他们被定义为

协方差

  • 协方差不接受超类型。
  • 协方差确实接受子类型。

逆变

  • 逆变确实接受超类型。
  • 逆变法不接受亚型。

但这并没有真正映射到我的脑海中只读/只写。任何人都可以更深入地解释为什么会这样吗?

javascript types flowtype
4个回答
4
投票

我不熟悉该语言的语法,所以这个答案是伪代码的。

想象一下,我们有三种类型,Siamese < Cat < Animal,并定义一个接口

interface CatCage {
    cat: Cat
}

并写一些方法

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

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

Covariance

如果我们使字段协变,我们可以定义一个像

SiameseCage < CatCage {
    cat : Siamese
}

但如果我们这样做

put_cat_in_cage (aCat, aSiameseCage)

在这种情况下,aSiameseCage.cat的价值是多少? SiameseCage认为它应该是一个Siamese,但我们只能使它成为一个Cat - 显然,该字段不能在界面上可写并且同时具有协变性。

Contravariance

如果我们使字段逆变,我们可以定义一个像

AnimalCage < CatCage {
    cat : Animal
}

但现在我们做不到

get_cat_in_cage (anAnimalCage)

由于anAnimalCage.cat的价值不能保证是Cat。因此,如果该字段是逆变的,则该字段在界面上是不可读的。

您可以通过返回Object或任何基本类型来使其在界面上可读,但可能没有任何实际用例,因此该语言在决定它时是明智的。


3
投票

既然你标记了这个,我就可以随意使用格拉斯哥扩展品种的一些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)

但是如果你尝试单翻,你会被卡住,因为没有办法从c派生出d


2
投票

逆变性仅意味着“在相反方向上变化”(并且协方差仅意味着“在相同方向上变化”)。在子类型关系的上下文中,它指的是当复合类型是另一种类型的子类型时的情况if-only-only-如果其中一个部分是另一种类型中相同部分的超类型。

“复合类型”我只是指具有其他组件类型的类型。像Haskell,Scala和Java这样的语言通过声明类型具有参数(Java称之为“泛型”)来处理此问题。简单地看一下Flow文档的链接,看起来Flow没有形式化参数,并且有效地将每个属性的类型视为一个单独的参数。因此,我将避免具体细节,只谈论由其他类型组成的类型。

子类型完全取决于可替代性。如果有人想要一个T,我可以给他们一个T任何子类型的值,什么都不会出错;他们被“允许”处理他们要求的东西的事情只是与任何可能的T有效的事情。当类型具有其他类型的子结构时,方差就会出现。如果有人要求一个结构包含组件类型T的类型,并且我想给它们一个具有相同结构但组件类型为S的类型的值,那么什么时候有效?

如果组件类型在那里,因为他们可以使用他们要求的对象获取T值(比如读取属性,或者调用返回T值的方法),那么当我给它们我的值时,他们将获得S值而不是他们期待的T值。他们想要用这些值做Tish事情,这只有在ST的子类型时才有效。因此,对于复合类型,我必须是他们想要的类型的子类型,我所拥有的那个组件必须是他们想要的组件的子类型。这是协方差。

另一方面,如果组件类型存在,因为他们可以将T值发送到他们要求的对象(比如写一个属性,或者调用一个以T值作为参数的方法),那么当我给它们我的值时它会期望他们发送它S值而不是T值。我的目标是想用其他人发送给它的S值做Tish事。如果TS的子类型,那只会起作用。所以在这种情况下,对于复合类型我必须是他们想要的类型的子类型,我所拥有的那个组件必须是他们想要的组件的超类型。这是相反的。


简单的函数类型是一个具体的例子,通常很容易理解。用Haskell表示法编写的函数类型就像ArgumentType -> ResultType;这本身是一个具有两种组件类型的复合类型,因此我们可以询问是否可以用一种函数类型替换另一种函数类型(是其子类型)。

假设我有一个Dog值的列表,我需要在它上面映射一个函数,将其转换为Cat值列表。所以执行映射的函数期望我给它一个类型为Dog -> Cat的函数。

我可以给它一个GreyHound -> Cat类型的功能吗?没有;映射函数将在列表中的所有Dog值上调用我的函数,并且我们不知道它们都是GreyHound值。

我可以给它一个Mammal -> Cat类型的功能吗?是;我的函数只能执行对任何Mammal有效的事情,这显然包括将被调用的列表中的所有Dog值。

我可以给它一个Dog -> Siamese类型的功能吗?是;映射函数将使用此函数返回的Siamese值来构建Cat列表,而Siamese值是Cat值。

我可以给它一个Dog -> Mammal类型的功能吗?没有;这个函数可能会将Dog变成Whale,它不适合映射函数需要构建的Cat列表。


2
投票

遇到方差的最常见位置是函数参数和返回值。函数在它们的参数中是逆变的,并且在它们的返回值中是协变的。

使用只读和只写变量来获得关于对称性的直觉的一种方法是从调用它的代码的角度考虑一个函数。从这个角度来看,参数是只写的:您将参数传递给函数,但该函数之外的代码不能找出您传递的内容或函数在内部处理它的类型。同样,返回值是只读的:当你调用一个函数时,它会给你一些东西而你又不能把它放回去。它给你的价值可能是你所期望的任何子类型。

任何只读的都是协变的,因为它允许给你比你要求的更多(子类型)。作为只读数据的用户,您只使用您期望的类型的功能,并忽略来自您实际获得的子类型的额外内容。

任何只写的东西都是逆变的,因为即使你给它确切的类型,它也可以选择不使用所有的功能,并将你给它的东西视为超类型。例如,错误记录器可能接受带有日期,错误代码等的复杂错误对象,但实际上委托给只记录消息的更简单的记录器。

© www.soinside.com 2019 - 2024. All rights reserved.