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?

  • I vote for Option 1.  I remember seeing it mentioned some time ago in some other blog entry discussing the CLR support for variance, and I thought it was a very sensible notation.  Also, I happen to be one of those people who often has to stop to think about which is which when it comes to covariance and contravariance, so I can personally vouch that Option 1 is the most intuitive (at least to me :)).

  • Does option 4 imply the generic parameters would be attributed?

    If not, how feasible is it to use attributes for co-/contra-variance?  For example:

    [Contravariant(parameter="T")]

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

    This has a couple of advantages, it's binary compatible with .NET 2.0 and the user is free to make an alias for ContravariantAttribute...

  • #3 and #4 are very nice because they are explicit - #3 esp. fits in with existing c# syntax.  maybe even something like: interface IFoo<T, U> where T is covariant, U is contravariant { …

    However like you said, the danger is that then everyone needs to learn what "covariant" and "contravariant" are - I think that is where option #2 - the Javaesque has the advantage...

  • I definitely do NOT like 5 - out is already a keyword and this would just lead to more confusion.

    Honestly, I don't have a good feeling for the others. I guess I would go with #1, with #3 being close behind. #4...did you mean attributes, or not? That's a bit confusing.

  • After my previous suggestion I am inclined towards #3. The verbosity is of a correct level correct, the names are googleable (don't underestimate the utility of this, especially when looking for code examples on the web) and actually correspond to the exact mechanism in use.

    Obviously the slight confusion is that they are no longer constraints they are freedoms :)

    There is the danger of more and more being expressed in the constraints/freedoms (especially if you add static operator constraints :) but I think it's cleaner than my initial suggestion so gets my strong vote.

    I am strongly -100 points out the door on any use of punctuation within general purpose modern languages languages*  (I preferred extends to : to be honest).

    The existing ; : ? at least all hail from C/C++ and thus get some serious positive points based on familiarity. This doesn't need a terse syntax (unlike the shortcuts for nullable type useage) and therefore shouldn't be used.

    Just my opinion of course :)

    *  strong assumption that a modern IDE is present for code completion. I know not everyone does but when a significant proportion do its reasonable to target to that population

  • I like option 1.  Though it may be difficult for others to remember what the + and - mean, there is precedent for this usage in other languages and notations.  Beyond Scala, I'm pretty sure OCaml uses this too.

    People are going to have to learn something new anyways.  It might as well be the common notation.

  • I agree with you about punctuation. I would prefer "extends" (for base classes) and "implements" (for interfaces) to the semantically ambiguous ":".

  • So far:

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

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

    ...

    I suggest:

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

    Let me use your example from an earlier post:

    Func<Animal, Giraffe> f1 = whatever;

    Func<Mammal, Mammal> f2 = f1;

    Plugging in types, Func<-A, +R> would become:

    Func<* is Animal, Giraffe is *>

    To see if Func<Mammal, Mammal> would be valid:

    Func<Mammal is Animal, Giraffe is Mammal>

    Yay, those "is" statements would equate to true!  Plus, one uses "is" to test assignability, which is exactly what this whole covariance and contravariance thing is about.  Now, let's test this against intuitiveness.  The programmer needs to know that when you have:

    delegate TypeA = delegate TypeB

    that the type parameters of TypeB get plugged into the type parameter names in the declaration (A & R in my example) and the type parameters of TypeB get plugged into the wildcards.  He/she would need to understand that you take what you know must be true (TypeB), plug those types into the type parameter names, and plug what you want to test (can TypeA hold TypeB?) into the wildcards.  Another way to say this is that A & R need to exist before * can exist.  

    A benefit of "is": it allows for "equals" and not just bigger than/smaller than, implements/extends, or A : B.

  • That's a great idea!

  • Yeah - Luke's idea certainly gets several positives to outweigh the -100. especially since * meaning wildcard is reasonably ingrained (and relatively platform agnostic)  thanks to globing.

    Gets my reflex vote but I haven't given it much detailed  thought yet

  • How about combining Luke's idea with option 3, ie:

    Func<T, U> where T is Animal, Mammal is U { ...

  • The danger of using the '*' wildcard is that the type definition in a generic is *already* a wildcard, to some extent.  Sure, it's a different kind of wildcard... but do you really want to be explaining to newbies about different kinds of type wildcards?  Ouch.

    Suggestion #5 gets points in this respect, but only a few: it's still not obvious when I should say "in T" vs. "T" unless I think for a very long time.

    To be honest, a lot of this covariance/contravariance stuff seems too complex.  People complain about extension methods and iterators and LINQ, but those all seems very simple to me once you've read an introduction about them.  But covariance/contravariance never gets any easier, and that should be a big warning sign.  Why not just make it really easy to create an adaptor instead?  (For example, C++ doesn't try to grapple with fancy covariance/contravariance issues, but it does have boost::bind, which is absolutely great to use and very easy to understand - at least in semantics.)

  • That should be

    interface IFoo<T, U> where T is Animal, Mammal is U { ...

    of course.

  • Funny how everyone else hates 5. I love it, I think it's the only option that makes the slightest bit of sense.

    The thing I find frustrating about this talk of variance is that it doesn't particularly help in the case I want variance for, though.

    I want to be able to declare a variable of type List<?> and then populate it by calling a method by reflection that I know returns a List<T> but I don't know what T is. And I want to be able to do that *without* having to have anything special on the List class to accomodate it - because I didn't write the List class in the first place. And if I have constraints on T I want to be able to express them too.

    Then I want to be able to access T-typed properties on that variable but just get them back as type "object" (or the appropriate constraining type).

    It shouldn't be necessary to still be using the non-generic IList and IEnumerable types all over the place just because we don't statically know the relevant "T". And when I define a generic interface, or class, I shouldn't have to jump through hoops to define non-generic versions of everything I make if I might ever want to use it on a reflection-instantiated class.

  • I seriously need to read and think before I press submit. It would have to be something like:

    interface IFoo<T, U> where T is *, * is U { T Foo(U u); }

Page 1 of 5 (66 items) 12345