Почему ковариация / контравариантность подразумевают только чтение / только запись? - PullRequest
0 голосов
/ 18 сентября 2018

Если вы посмотрите на потоковые документы по ковариантным / контравариантным полям в интерфейсах, ковариантность подразумевает только чтение, а контравариантность - только запись. Однако я не очень понимаю, почему. В своих документах по дисперсии они определены как

ковариации

  • Ковариация не принимает супертипы.
  • Ковариация принимает подтипы.

контрвариация

  • Контравариантность принимает супертипы.
  • Контравариантность не принимает подтипы.

Но это на самом деле не означает, что я могу читать только для чтения / только для записи. Может ли кто-нибудь объяснить более подробно, почему это так?

Ответы [ 4 ]

0 голосов
/ 20 сентября 2018

Самое распространенное место, где вы сталкиваетесь с дисперсией, это аргументы функций и возвращаемые значения.Функции контравариантны в своих аргументах и ​​ ковариантны в своих возвращаемых значениях.

Чтобы получить представление о симметрии с переменными только для чтения и только для записи, нужно подумать о функции с точки зрения кода, который ее вызывает.С этой точки зрения аргументы только для записи : вы передаете аргументы функции, но ни один код вне этой функции не может узнать, что вы передали или какого типа функция обрабатывает ее как внутреннюю.Аналогично, возвращаемые значения только для чтения : когда вы вызываете функцию, она дает вам что-то, и вы не можете вернуть ее обратно.И ценность, которую он вам дал, могла быть любым подтипом того, что вы ожидали.

Все, что доступно только для чтения, является ковариантным, поскольку разрешено давать вам больше, чем вы просили (подтип).Как пользователь данных только для чтения, вы используете только функциональность ожидаемого вами типа и игнорируете дополнения из того подтипа, который вы фактически получили.

Все, что доступно только для записи, противоречивопотому что даже если вы дадите ему точный тип, который он запрашивал, он может выбрать не использовать все функции и рассматривать то, что вы ему дали, как если бы он был супертипом.Например, регистратор ошибок может принять сложный объект ошибки с датами, кодами ошибок и т. Д., Но фактически делегировать более простому регистратору, который только записывает сообщение.

0 голосов
/ 18 сентября 2018

Я не знаком с синтаксисом языка, поэтому этот ответ в псевдокоде.

Представьте, что у нас есть три типа 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
}

Ковариация

Если мы сделаем поле ковариантным, мы можем определить экземпляр типа

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)

Asзначение anAnimalCage.cat не обязательно будет Cat.Таким образом, поле не может быть читаемым на интерфейсе, если оно контравариантно.

Вы могли бы потенциально сделать его читаемым на интерфейсе, возвращая Object или любой другой базовый тип, но это, вероятно, не будетиметь реальный вариант использования, поэтому язык разумно примет решение против него.

0 голосов
/ 19 сентября 2018

Контравариантность просто означает «меняется в противоположном направлении» (а ковариация означает просто «меняется в одном и том же направлении»).В контексте отношений подтипов это относится к случаям, когда составной тип является подтипом другого типа, если-и-только-если одна его часть является супертипом той же части в другом типе.

Под «составным типом» я подразумеваю тип, имеющий другие типы компонентов.Такие языки, как Haskell, Scala и Java, обрабатывают это, заявляя, что у типа есть параметры (Java называет это «универсальными»).Из краткого обзора ссылки на документы Flow видно, что Flow не формализует параметры и фактически рассматривает тип каждого свойства как отдельный параметр.Поэтому я избегу специфики и просто расскажу о типах, состоящих из других типов.

Подтипирование - это все о заменяемости.Если кто-то хочет T, я могу дать ему значение любого подтипа T, и ничто не пойдет не так;то, что им «разрешено» делать с тем, о чем они просили, - это только то, что допустимо делать с любым возможным T.Дисперсия возникает, когда типы имеют подструктуру других типов.Если кто-то запрашивает тип со структурой, включающей тип компонента T, и я хочу дать ему значение с типом, который имеет такую ​​же структуру, но тип компонента равен S, когда это допустимо?

Если тип компонента существует, потому что они могут получить T значений, используя запрашиваемый объект (например, чтение свойства или вызов метода, который возвращает значения T),затем, когда я передам им свою ценность, они получат S значений из нее вместо T значений, которые они ожидали.Они захотят сделать что-то с этими значениями T, что будет работать, только если S является подтипом T.Так что для составного типа я должен быть подтипом того, который они хотят, компонент того, который у меня есть, должен быть подтипом компонента в том, что они хотят.Это ковариация .

С другой стороны, если есть тип компонента, потому что они могут отправлять T значений объекту, который они запрашивают (наподобие написания свойства или вызова метода, который принимает T значений в качестве аргументов), то когда я передам им свое значение, он будет ожидать, что они отправят ему S значения вместо T единиц.Мой объект захочет сделать S вещи со значениями T, которые другой человек отправит ему.Это будет работать, только если T является подтипом S.Таким образом, в этом случае для составного типа I должен быть подтип того, который им нужен, компонент того, который у меня есть, должен быть супертипом компонента в одномони хотят.Это Контравариантность .


Простые типы функций - это конкретный пример, который обычно легко понять, не задумываясь.Тип функции, написанный в нотации 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, который требуется построить функции отображения.

0 голосов
/ 18 сентября 2018

Поскольку вы пометили этот , я не стесняюсь использовать немного 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).Я могу легко прочитать a 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.

...