Covariance and Contravariance in C#, Part Eight: Syntax Options

Covariance and Contravariance in C#, Part Eight: Syntax Options

Rate This
  • Comments 66

As I discussed last time, were we to introduce interface and delegate variance in a hypothetical future version of C# we would need a syntax for it. Here are some possibilities that immediately come to mind.

Option 1:

interface IFoo<+T, -U> { T Foo(U u); }

The CLR uses the convention I have been using so far in this series of “+ means covariant, - means contravariant”. Though this does have some mnemonic value (because + means “is compatible with a bigger type”), most people (including members of the C# design committee!) have a hard time remembering exactly which is which.

This convention is also used by the Scala programming language.

Option 2:

interface IFoo<T:*, *:U> { …

This more graphically indicates “something which is extended by T” and “something which extends U”.  This is similar to Java’s “wildcard types”, where they say “? extends U” or “? super T”.

Though this isn’t terrible, I think it’s a bit of a conflation of the notions of extension and assignment compatibility. I do not want to imply that IEnumerable<Animal> is a base of IEnumerable<Giraffe>, even if Animal is a base of Giraffe. Rather, I want to say that IEnumerable<Giraffe> is convertible to IEnumerable<Animal>, or assignment compatible, or some such thing. I don’t want to conceptually overwork the inheritance mechanism. It's bad enough IMO that we conflate base classes with base interfaces.

Option 3:

interface IFoo<T, U> where T: covariant, U: contravariant { …

Again, not too bad. The danger here is similar to that of the plus and minus: that no one remembers what “contravariant” and “covariant” mean. This has the benefit at least that you can do a web search on the keywords and get a reasonable explanation.

Option 4:

interface IFoo<[Covariant] T, [Contravariant] U>  { …

Similar to option 3.

Option 5:

interface IFoo<out T, in U> { …

We are taking a different tack with this syntax. In all the options so far we have been describing how the user of the interface may treat the interface with respect to the type system rules for implicit conversions – that is, what are the legal variances on the type parameters. Here we are instead describing this in the language of how the implementer of the interface intends to use the type parameters.

I like this one a lot; the down side of this is of course that, as I described a few posts ago, you end up with situations like

delegate void Meta<out T>(Action<T> action);

where the "out" T is clearly used in an input position.

Option 6:

Do something else I haven’t thought of. Anyone who has bright ideas, please leave comments.

Next time: what problems are introduced by adding this kind of variance?

  • Personally, I like #1 and #5. I haven't read the comments... perhaps some much smarter syntax was introduced there.

  • "But covariance and contravariance are not simple things."

    Yes they are, or at least, they certainly should be.  The +/- syntax is far and away the best.  I think it corresponds nicely to the "or bigger (more general)" and "or smaller (less general)" concept, it's already used in other languages, and it's already used in the spec.

    I don't really understand what you might be doing where you want use-site Java-style wildcarding more than covariant and contravariant generic arguments.

  • Instead of [in] and [out] or [+] and [-] we may have a keyword that describe the "bigness"/"smallness" of covariance/contravariance-ness.  When you explained the terms you have explained it using the "Big" metaphor, similarly we can use keywords like [encompasses] and [encompassed].  Those keywords may be long 11 characters differing by the last letter "s" and "d", though I like the concept.  [surrounds] and [surrounded] are 9 and 10 characters, better but not ideal.  However, we can expand and as of now my favorite is: [enfold] and [strip].  They are different enough that they will not cause likely mistakes even with dyslexic folks like me who can see "North" and interpret "South" when driving on the highway.  Thank you for your attention --Avi Farah

  • I have not yet read the whole bunch of comments and suggestions, but i would prefer something like this:

    delegate R Func<atleast A, atmost R> (A a)

    It has the benefit that it avoids the fancy "*", plus it is easy readable once the programmer understood the concept of inheritance and specializing through inheritance.

  • delegate R Func<_ is A, R is _> (A a)

    is good

    another option:

    delegate R Func<A, R> (A a)

      where R>A

  • So nicely step by step blogged by Eric Lippert for &quot;Covariance and Contravariance&quot; as &quot;Fabulous

Page 5 of 5 (66 items) 12345