Hacker News new | past | comments | ask | show | jobs | submit login

> 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;;
See https://blogs.janestreet.com/a-and-a/ for a nice explanation.


> Therefore I doubt if it is really needed

Well the article seems to suggest that it absolutely is needed for function arguments.


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))
where the lambda has (inferred) type B -> 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.


Except that in many languages you have also input/ouput parameters that should be invariant, I agree.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: