> People tend to find covariance more intuitive than contravariance; unless the issue is pointed out, they tend to assume everything is covariant.
I can easily understand where covariance is useful and meaningful while contravariance is somehow counterintuitive for me.
Therefore I doubt if it is really needed (except for being a nice formal counterpart of covariance). In other words, would it be more natural to simply use covariance as a default rule while contravariance either being not available at all or used as a kind of exception (where you need to know what you are doing and understand the consequences)?
The most common cases are that anything that "receives" or "absorbs" is contravariant, while anything that "emits" or "produces" is covariant. If you're working with streams, Source is covariant and Sink is contravariant. A read-only collection is covariant, a write-only collection (sounds stupid I know, but it can be a good way to model e.g. an audit log) is contravariant. A function A => B is covariant in B but contravariant in A.
Contravariance is probably the least common, sure, but covariance should probably be opt-in too. A lot of languages treat things as covariant-by-default even for mutable types that are actually invariant, and so suffer from some variant of this unsoundness bug:
val dogs: List[Dog] = List[Dog]()
val animals: List[Animal] = dogs
val cat: Cat = Cat()
animals.append(cat)
val dog: Dog = dogs(0) // dog is now a Cat
If you ever want function types you will always need both co- and contravariance as pointed out in OP's articles.
Using covariance as a default is also not sound. Many things are "invariant", meaning neither co- or contravariant. This is already a sensible default. (It is however possible to automatically derive either property in some cases)
Both are just as common. The most trivial cases are getters (covariant) and setters (contravariant).
A getter which returns a dog is obviously a sub type of a getter which returns an arbitrary animal since the returned dog can do everything that an animal can. However it is different for setters, a setter which consumes an animal can also consume dogs, hence it is a subtype of a setter which consumes dogs. The reverse isn't true since a setter which consumes dogs can't also take animals which means that you can't pass it as an animal setter.
So we could (by default) assume that all getter (returns) are invariant, and all setter (arguments) are contravariant. If we forget about invariance for a moment then this means essentially that there is only one default pattern and hence no need in explicitly distinguishing and annotating covariance and contravariance - everybody simply knows this default rule. (I am simply reasoning after reading the article - I do not know if it can be done or makes sense.)
It can mostly be done. The OCaml programming language does exactly this - it infers co/contravariance. However, when describing a generic interface which has as part of it a function on types (usually called a "type constructor"), you need to say whether that type constructor is supposed to be covariant, contravariant, or invariant.
For example, if I were describing a sequence interface, I would write
module type SEQ =
sig
(* the "+" means that seq must be covariant *)
type +'a seq
val empty : 'a seq
val singleton : 'a -> 'a seq
val append : 'a seq -> 'a seq -> 'a seq
val lookup : int -> 'a seq -> 'a option
exception IndexOutOfBounds
end;;
It is not essential, and neither is covariance. For instance, C and C++ function types are invariant (but C++ virtual member functions are properly variant, and so is std::function). If a language does not support variance, it can always be emulated with wrapper functions. Eg. given types A :> B :> C
fn foo: (B -> B) -> ()
fn bar: A -> C
foo(b -> bar(b))
You are right, I probably did not express my idea clearly. What I meant is that there is only one meaningful pattern: returns are covariant and arguments are contravariant. If it is so (and the articles seems to confirm this) then why do we need special annotations? The system could itself assume that any argument (by default) is contravariant and any return (by default) is covariant. Does it make sense?
> The system could itself assume that any argument (by default) is contravariant and any return (by default) is covariant.
I guess languages could infer variance for generic parameters on methods/functions, and perhaps should - it would align with how type systems generally work. But that's a small part of the use case - variance mainly matters when you have generic values. If we have a Frobnicator[A, B] it would be very difficult for the language to infer whether variance for frobnicator values should run like for A => B, like for B => A, or some other way.
I can easily understand where covariance is useful and meaningful while contravariance is somehow counterintuitive for me.
Therefore I doubt if it is really needed (except for being a nice formal counterpart of covariance). In other words, would it be more natural to simply use covariance as a default rule while contravariance either being not available at all or used as a kind of exception (where you need to know what you are doing and understand the consequences)?