【问题标题】:Why do covariance/contravariance imply read-only/write-only?为什么协变/逆变意味着只读/只写?
【发布时间】:2019-02-21 22:37:48
【问题描述】:

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

协方差

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

逆变

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

但在我看来,这并没有真正映射到只读/只写。谁能更深入地解释为什么会这样?

【问题讨论】:

  • 这与 Haskell 函数式编程有什么关系?
  • @dfeuer 协变和逆变来自范畴论,因此我认为一般了解 Haskell 或 FP 的人比我只用flowtype 标记它更有资格回答这个问题,几乎没有超过 1k 的追随者。同样,了解 Haskell 的人通常对类型理论了解更多。
  • @m0meni Haskell 没有普遍的子类型化,这就是在通用 OOP 语言中差异如此重要的原因,其中子类型化是这片土地的法则。另外,我不知道与范畴论有任何实质性联系。我认为,“协变”和“逆变”这两个词在范畴论之前就已经存在了很长时间。它们只是意味着“以相同的方式改变/改变/跟随”和“以其他方式改变/改变/发散”。协变/逆变函子之间存在联系,但翻译需要一些工作。
  • 我认为在这种情况下,逆变和协方差意味着en.wikipedia.org/wiki/…。在 haskell 中,它们的含义完全不同 (fpcomplete.com/blog/2016/11/covariance-contravariance)

标签: javascript types flowtype


【解决方案1】:

既然你标记了这个 ,我会随意使用一些 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

【讨论】:

  • 我会把自己描述为“了解 Haskell”,但我觉得这很难阅读;我想有人在寻找为什么这普遍适用而不是特别适用于 haskell 的人将很难遵循这个论点
  • @Cubic,说实话,我当时心情很傻。
  • @Cubic,这在量化约束下也会更好(或不那么可怕),但这仍然是非常前沿的。
【解决方案2】:

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

假设我们有三种类型,Siamese &lt; Cat &lt; 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
}

协方差

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

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 或任何基本类型来使其在界面上可读,但这可能没有任何实际用例,因此该语言在决定反对它时是明智的。

【讨论】:

    【解决方案3】:

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

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

    子类型是关于可替代性的。如果有人想要T,我可以给他们一个T 的任何子类型的值,不会出错;他们“被允许”做的事情只是对任何可能的T 有效的事情。当类型具有其他类型的子结构时,就会出现差异。如果有人要求一个结构包含组件类型T的类型,我想给他们一个具有相同结构但组件类型为S的类型的值,那什么时候有效?

    如果组件类型存在,因为他们可以使用他们要求的对象获得T 值(例如读取属性,或调用返回T 值的方法),然后当我给他们我的价值时,他们会从中得到S 值,而不是他们期望的T 值。他们会想用这些值来做Tish 的事情,这只有在ST 的子类型时才有效。所以对于复合类型,我必须是他们想要的那个的子类型,我拥有的那个的组件必须是他们想要的那个组件的子类型。这是协方差

    另一方面,如果组件类型存在,因为它们可以发送 T 值到他们请求的对象(例如编写属性,或调用采用 @ 的方法987654334@ 值作为参数),然后当我给他们我的值时,它会期望他们发送S 值而不是T 值。我的对象会想用其他人要发送给它的T 值来做Sish 的事情。这只有在TS 的子类型时才有效。所以在这种情况下,对于复合类型,我必须是他们想要的复合类型的 子类型,我拥有的复合类型的组件必须是组件的 超类型他们想要的那个。这是逆变


    简单的函数类型是一个具体的例子,通常只要稍加思考就很容易理解。用 Haskell 表示法编写的函数类型类似于 ArgumentType -&gt; ResultType; this 本身是一个有两个组件类型的复合类型,所以我们可以询问一个函数类型是否可以替代另一个函数类型(是它的子类型)。

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

    我可以给它一个GreyHound -&gt; Cat 类型的函数吗?不;映射函数将对列表中的所有Dog 值调用我的函数,我们不知道它们都是GreyHound 值。

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

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

    我可以给它一个Dog -&gt; Mammal 类型的函数吗?不;此函数可能会将Dog 转换为Whale,它不适合映射函数需要构建的Cat 列表。

    【讨论】:

      【解决方案4】:

      您遇到方差最常见的地方是函数参数和返回值。函数的参数逆变,返回值协变

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

      任何只读的东西都是协变的,因为它可以给你比你要求的更多的东西(一个子类型)。作为只读数据的用户,您只使用您期望的类型的功能,而忽略来自您实际获得的子类型的额外功能。

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

      【讨论】:

        【解决方案5】:

        考虑基本类型 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.
        

        【讨论】:

          猜你喜欢
          • 2011-05-28
          • 1970-01-01
          • 2020-09-06
          • 2013-10-18
          • 2019-03-12
          • 2015-06-18
          • 2019-05-24
          • 1970-01-01
          • 2010-10-22
          相关资源
          最近更新 更多