Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Covariance and contravariance in subtyping (thegreenplace.net)
64 points by ingve on Oct 18, 2018 | hide | past | favorite | 13 comments


As good an explanation for the subject as I've seen.

Drive-by repetition of my general curmudgeon opinion: the point at which a real-world software design finds it needs to worry about the specifics of c*variant behavior in its types is precisely the point at which a real-world software design becomes an inappropriate fit for language-based subtyping. Worrying about this stuff hurts rather than helps, if you're trying to solve real problems.

Fight me. As it were.


Do you consider libraries "Real-world software design"? Because libraries are where this kind of stuff really matters. Getting it right makes your library easily reusable. Getting it wrong makes all reuse a pain.


You're using "reuse" in a paritucularly narrow sense to mean "usable across the local language's natural type spectrum". And yes, getting that (ahem) "right" is a huge honking pain without careful attention to subtype variance. It's just that you view that as a feature to solve a problem and I view it as a symptom of having tried to solve the wrong problem[1] in the first place.

There are lots of ways to have perfectly "reusable" code that doesn't involve subtyping.

[1] Because let's be honest: modern "library" code consists of maybe 72% of effort expended on solving the wrong problem.


I was mostly thinking of e.g. the C++ Standard library, or a TCP library, or a multithreading framework.

And really, by re-using that code I mean using the library. Now, most `libraries` don't fall under this category. But for libraries that are truly going to be used, especially if they are used in multiple different organizations, separate from the library writers, then this matters.

To your point though, these library writers aren't delivering a what a lot of people think of as a `software product`. I would call it more of an intermediate-product than an end-product.


There might be pockets in a software design where it's appropriate to worry about c*variant behavior. However, if you end up with a brittle class hierarchy that affects modularisation, dependency management or reuse then I would agree that using type relationships to model your domain is a losing battle.


Java Arrays.


Covariance and contravariance is often discussed in terms of subtyping, but it goes deeper than this. Generic types can be covariant or contravariant in languages without subtyping too. The List<T> type is covariant because it supports a map operation: given a function A -> B and a List<A> you can get a List<B>. That phenomenon in mathematics is where the terms covariance and contravariance came from. The covariance you see in OO languages is the special case when the function A -> B is an upcast from the subtype A to the supertype B.


No list<> is not covariant (it’s even mentioned that collections aren’t covariant in the article).

Your example, map() is a function that takes list<a>, and a function of type a->b, and uses that to produce a value of type list<b>.

Note that at no point is a value of type List<A> ever treated as a value of type List<B>

You may be confused by how member syntax looks in OO languages. What you need to realize that List<T>::map<B>(A->B) is an alias to a function along the lines of

List_map<A,B>(List<A>, A->B)->List<B>

Again there’s no actual subtyping relationship implied between list<a> and list<b>


List::map vs List_map is merely a syntactic difference. That's not the issue.

Mutable collections are invariant, immutable collections are covariant. Mutable collections do not support map in the mathematical sense, immutable collections do.

I didn't say that if you map List<A> to List<B> that List<B> is then a subtype of List<A>. That is only the case if A is a subtype of B. Rather, the ability to write map implies that List is covariant, and conversely, covariant types support map.

A type Foo<T> is covariant if T appears only as an output to methods and not as an input to methods. For such types you can implement map(f) by returning a proxy that calls the original object, but whenever a method returns a value v of type T you return f(v) instead.


This was debated in the 90s , Bertrand Meyer (Eiffel) has a good example how boys and girls would end up sharing rooms on a ski trip if variance is handled incorrectly:

https://archive.eiffel.com/doc/manuals/technology/typing/pap...

It was an interesting time - Java didn't have generics yet and C# didn't exist. OO purists were complaining about C++ and Java implementing subclassing using subtyping but in the end no language really directly supports something like the Liskov substitution principle, which is about behavior and not just substituting function signatures.


I'm not sure LSP can be formally enforced unless you encode the contracts of each methods and prove they are held. This is probably doable in many cases, but the current tools seem pretty heavyweight, as is the mental burden.

Regarding these years, I very much regret that subclassing won over interfaces / traits / typeclasses without inheritance.


True but in statically typed languages we have a lot of options now. It's interesting to see how c# and C++ have developed and what approaches new languages are taking. My own practices have definitely changed over the last 25 years, where I initially used class hierarchies to model my problems I now think in terms of interfaces. Also, with everything being connected I just don't worry about building large monolithic systems composed of class libraries any more.


> If you had to guess which of the mainstream languages has the most advanced support for variance in their type system, Python probably wouldn't be your first guess, right?

When the author wrote this statement, I think they didn't consider type bounds in Java - for example:

    void addAll(Collection<? extends E> coll)
    void getAll(Collection<? super E> coll)
Don't forget this (ahem) beauty:

    <T extends Comparable<? super T>> void sort(List<T> list)




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

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

Search: