Why do covariance/contravariance imply read-only/write-only?
Asked Answered
R

5

6

If you take a look at the flow docs on covariant/contravariant fields in interfaces, covariant implies read-only and contravariance implies write-only. However, I don't really understand why. In their docs on variance, they're defined as

Covariance

  • Covariance does not accept supertypes.
  • Covariance does accept subtypes.

Contravariance

  • Contravariance does accept supertypes.
  • Contravariance does not accept subtypes.

But that doesn't really map to read-only/write-only in my mind. Could anyone explain more in depth why it's the case?

Relational answered 18/9, 2018 at 0:26 Comment(4)
What does this have to do with Haskell or functional programming?Passage
@Passage covariance and contravariance come from category theory, and thus I figured that people that know Haskell or FP in general would be more qualified to answer this question than if I just tagged it with flowtype, which barely has over 1k followers. In the same vein, people who know Haskell in general know a lot more about type theory.Relational
@Relational Haskell doesn't have pervasive subtyping though, which is what makes variance so important in generic OOP languages, where subtyping is the law of the land. Also, I am not aware of any substantial connection to category theory. The words "covariant" and "contravariant", I think, have existed for a long time before category theory. They just mean "varying with/changing in the same way/following" and "varying against/changing in the other way/diverging". There is a connection in covariant/contravariant functors, but it takes a bit of work to translate.Philippines
I think in this context Contravariance and Covariance mean this en.wikipedia.org/wiki/…. In haskell, they mean something entirely different (fpcomplete.com/blog/2016/11/covariance-contravariance)Slap
S
6

I'm not familiar with the syntax of the language, so this answer is in pseudo-code.

Imagine we have three types, Siamese < Cat < Animal, and define an interface

interface CatCage {
    cat: Cat
}

and write some methods

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

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

Covariance

If we make the field covariant, we can define an instance like

SiameseCage < CatCage {
    cat : Siamese
}

But if we do

put_cat_in_cage (aCat, aSiameseCage)

What is the value of aSiameseCage.cat in this instance? SiameseCage thinks it ought to be a Siamese, but we've just been able to make it a Cat - clearly, the field can't be writeable on the interface and be covariant at the same time.

Contravariance

If we make the field contravariant, we can define an instance like

AnimalCage < CatCage {
    cat : Animal
}

But now we can't do

get_cat_in_cage (anAnimalCage)

As the value of anAnimalCage.cat isn't guaranteed to be a Cat. So the field can't be readable on the interface if it's contravariant.

You could potentially make it readable on the interface by returning an Object, or whatever the base type is, but that probably wouldn't have any real use case, so the language is sensible in deciding against it.

Squirm answered 18/9, 2018 at 17:21 Comment(0)
A
4

The most common place that you come across variance is in function arguments and return values. Functions are contravariant in their arguments and covariant in their return values.

A way to get intuition about the symmetry with read-only and write-only variables is to think about a function from the perspective of code that is calling it. From this perspective, arguments are write-only: you pass arguments to a function but no code outside of that function can find out what you passed or what type the function treats it as internally. Similarly, return values are read-only: when you call a function it gives you something and you can't put it back. And the value it gave you could be any sub-type of what you expected.

Anything that is read-only is covariant because it is allowed to give you more than you asked for (a sub-type). As a user of the read-only data, you use only the functionality of the type you expected and ignore the extras that are from the sub-type that you actually got.

Anything that is write-only is contravariant because even if you give it the exact type that it asked for, it can choose not to use all of the functionality, and treat what you gave it as if it was a super-type. For example, an error logger might accept a complicated error object with dates, error codes etc, but actually delegate to a simpler logger that only records the message.

Assembled answered 20/9, 2018 at 0:29 Comment(0)
P
3

Since you tagged this , I'll feel free to use some Haskell... of the Glasgow extended variety.

{-# 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

Suppose I have an IORef (Foo c). I can easily read a Foo d from it:

readDFromC :: (forall a. c a :- d a) -> IORef (Foo c) -> IO (Foo d)
readDFromC cd ref = upcast cd <$> readIORef ref

Similarly, I can do a double flip, replacing a Foo d by a 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)

But if you try the single flips, you'll be stuck because there's no way to derive c from d.

Passage answered 18/9, 2018 at 0:53 Comment(3)
I would've described myself as "knowing haskell" and I find this quite hard to read; I imagine someone looking for why this holds in general rather for haskell in particular is going to have a pretty hard time following this argumentSamalla
@Cubic, I was in a silly sort of mood, to be honest.Passage
@Cubic, this will also be a bit nicer (or less horrible) with quantified constraints, but that's still pretty bleeding edge.Passage
P
3

Contravariance just means "varies in the opposite direction" (and covariance just means "varies in the same direction"). In the context of subtype relationships, it refers to cases when a compound type is a subtype of another type if-and-only-if one its parts is a supertype of the same part in the other type.

By "compound type" I just mean a type that has other component types. Languages like Haskell, Scala, and Java handle this by declaring that a type has parameters (Java calls this "generics"). From a brief look at the link to the Flow docs, it looks like Flow doesn't formalise parameters, and is effectively considering the type of each property to be a separate parameter. So I'll avoid specifics and just talk about types that are made up of other types.

Subtyping is all about substitutability. If someone wants a T, I can give them a value of any subtype of T, and nothing will go wrong; the things they're "allowed" to do with the thing they asked for are only the things that are valid to do with any possible T. The variance comes in when the types have substructure of other types. If someone asks for a type with a structure that includes a component type T, and I want to give them a value with a type that has the same structure but the component type is S, when is that valid?

If the component type is there because they can obtain T values using the object they're asking for (like reading a property, or calling a method that returns T values), then when I give them my value they'll be getting S values out of it instead of the T values they were expecting. They'll want to do Tish things with those values, which will only work if S is a subtype of T. So for the compound type I have to be a subtype of the one they want, the component of the one I have must be a subtype of the component in the one they want. This is covariance.

On the other hand, if the component type is there because they can send T values to the object they're asking for (like writing a property, or calling a method that takes T values as arguments), then when I give them my value it will be expecting them to send it S values instead of T ones. My object will want to do Sish things with the T values the other person is going to send to it. That's only going to work if T is a subtype of S. So in this case, for the compound type I have to be a subtype of the one they want, the component of the one I have must be a supertype of the component in the one they want. This is contravariance.


Simple function types are a concrete example that is generally easily understood with a little thought. A function type written in Haskell notation is like ArgumentType -> ResultType; this itself is a compound type with two component types, so we can ask whether one function type can be substituted for (is a subtype of) another function type.

Lets say I've got an list of Dog values, and I need to map a function over it to turn it into a list of Cat values. So the function that does the mapping is expecting me to give it a function of type Dog -> Cat.

Can I give it a function of type GreyHound -> Cat? No; the mapping function will call my function on all of the Dog values in the list, and we don't know that they are all GreyHound values.

Can I give it a function of type Mammal -> Cat? Yes; my function can only do things that are valid for any Mammal, which obviously includes all of the Dog values in the list that it will be called on.

Can I give it a function of type Dog -> Siamese? Yes; the mapping function will use the Siamese values returned by this function to build a list of Cat, and Siamese values are Cat values.

Can I give it a function of type Dog -> Mammal? No; this function might turn a Dog into a Whale, which won't fit in the list of Cat the mapping function needs to build.

Petrography answered 19/9, 2018 at 1:38 Comment(0)
O
0

Consider base type Animal and subtypes Dog and Cat.

Consider writing to be feeding, and reading to be looking at fur.

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.
Owenism answered 12/4, 2021 at 21:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.