Covariance and Contravariance in C#, Part Eight: Syntax Options
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?
Comments
Anonymous
October 31, 2007
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 :)).Anonymous
October 31, 2007
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...Anonymous
October 31, 2007
The comment has been removedAnonymous
October 31, 2007
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.Anonymous
October 31, 2007
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
Anonymous
October 31, 2007
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.Anonymous
October 31, 2007
I agree with you about punctuation. I would prefer "extends" (for base classes) and "implements" (for interfaces) to the semantically ambiguous ":".Anonymous
October 31, 2007
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.Anonymous
October 31, 2007
That's a great idea!Anonymous
October 31, 2007
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 yetAnonymous
October 31, 2007
How about combining Luke's idea with option 3, ie: Func<T, U> where T is Animal, Mammal is U { ...Anonymous
October 31, 2007
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.)Anonymous
October 31, 2007
That should be interface IFoo<T, U> where T is Animal, Mammal is U { ... of course.Anonymous
October 31, 2007
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.Anonymous
October 31, 2007
The comment has been removedAnonymous
October 31, 2007
The comment has been removedAnonymous
October 31, 2007
apenwarr, I do not think newbies will be exposed to covariance and contravariance [knowingly]. This will be useful to people who like to take the language to the limits and people who write frameworks. Maybe I'm wrong, but I think support for covariance and contravariance will show itself by things working more naturally, because people will use code that allows for variance more often than write it. Eric, I don't recall seeing any real-life, motivating examples for this where such support would make code significantly more readable (or somewhat more readable in many places). In other words, what scores points against the -100? If this is coming up in a future post, ignore this question. :-)Anonymous
October 31, 2007
With the "is" keyword, would we really even need the ""? To me it seems like unnecessary typing, and I'm a hprrbile tpyer so the less typing the better. In my eyes, "delegate R Func<is A, R is> (A a)" conveys just as much information as "delegate R Func< is A, R is *> (A a)" But so far, I like that general idea the best, asterisk or not.Anonymous
October 31, 2007
My previous comment didn't seem to go though, so forgive me if this ends up getting posted twice. Option 3 should be : interface IFoo<T, U> where T: covariant [where] U: contravariant { … to be consistant with existing constraints. Commas only seperate constraints on a single parameter. I would also like to suggest you avoid confusion between generic constraints and 'freedoms', using a 'let' clause. interface IFoo<T, U> where T: IBar //a constraint on T let T: covariant //a 'freedom on T where U: new() //a constraint on U let U: contravariant //a freedom on U { … basically, seperate out constraints (where) from freedoms (let) in a LINQ like declaritive way. This is better IMO than putting anything in between <>'s because it fits more with the current constraint model (just where clauses) -BrandonAnonymous
October 31, 2007
I think the description of U in Option 2 is reversed. The post says that ":U" means “something which U extends”. Shouldn't that be “something which extends U”? The description of "T:" could also be simplified to “something which T extends”.Anonymous
October 31, 2007
Freedoms are certainly the most palatable solution. Whether 'covariant' and 'contravariant' are the most intuitive keywords is debatable. How about: interface IFoo<T, U> where T : IBar let * : T where U : IStuff let U : * { ... The relatively universal wildcard '*' still denotes 'any type where' and the freedoms provide a clean, extensible syntax. My only reservation is using ':' to denote a non-inheritance relationship. How about this even: nterface IFoo<T, U> where T : IBar let * >= T where U : IStuff let U <= * { ...Anonymous
October 31, 2007
my vote is for lukes idea for readability; it's great. a secondary vote for the attribute approach due to backwards compat and further 'verbose-ness'. it does allow searching for the concepts which is great. but out of all; i prefer lukes. great idea. it actually helped me understand the concept incredibly easily.Anonymous
October 31, 2007
I am wavering on the "constraint/freedom" question. Saying 'U may be anything like this' is equivalent to saying 'U cannot be anything out of this range', so I would still consider variance a constraint, not a freedom. From that perspective, I like "assignment constraints": where T : IBar where T <= * where U : IStuff where U >= * Check for a maximum of 1 inheritance constraint and 1 assignment constraint.Anonymous
October 31, 2007
The comment has been removedAnonymous
October 31, 2007
The comment has been removedAnonymous
October 31, 2007
Last comment of the night, I promise. But I just want to emphasize how important I feel this is. I've been begging for variance in generics for a very long time and very strenuously. But if all you're going to do is the delegate/interface variance you've been talking about, no matter how intuitive you manage to make the syntax, I'd actually argue strongly against bothering to include it at all. It adds a lot of complexity and helps in pretty much zero of the scenarios where I've encountered a need for variance in real life. If you can't offer use-site variance, just stick to covariant return types on overridden methods, and beyond that, don't even bother.Anonymous
October 31, 2007
The following Microsoft Research is an interesting read: "Variance and Generalized Constraints for C# Generics" http://research.microsoft.com/research/pubs/view.aspx?type=inproceedings&id=1215 And it discusses variant classes. While it would be useful, at least variant interfaces would be a great start which hopefully could be extended in the future.Anonymous
October 31, 2007
How about using base, it already has some meaning relevant to this: delegate R Func<base A, R base> (A a) So the position of base indicates the co/contraAnonymous
October 31, 2007
Marc, All your base are belong to us.Anonymous
October 31, 2007
Option 1 (+/-) looks good enough for me. minus by minus equals plus in general math too, this is a benefit of this syntax. Option 2 (T:/:U) confuses me. Like Bradley, I thought it to be reversed, and I still don't understand how :U (or ? extends U, for that matter) expresses "something which U extends". It's also hard to relate "something which U extends" to "interface is contravariant on U"... Option 3 (where "constraints"): I share the reservations about using where for constraints AND variance. Option 4 (Attributes): good enough. + and - might be easier to relate to bigger/smaller types, but co/contravariance are easier to google. tough to decide, i suppose either one will do. I'd perfer a contextual keyword to attributes though. this is a first class language feature after all! Option 5 (in/out): I see how you like this for simple cases, but get real: it's incorrect for others, and at the end of the day, you're not going to include incorrect syntax into the language, even if we'd all love it. (which I do not, for exactly this reason) Luke's suggestion ( is A) and Matt's one (positional "base"): I find those just as confusing as option 2. Rasmus's suggestions indicates that he didn't understand the problem, the "where" syntax led him to believe that we're talking about constraints (or maybe I'm not getting it). Goes to prove that option 3 is a bad idea. Actually I like Peter Ritchie's suggestion a lot, only that I would recommend built-in syntax: interface IFoo<T,U> covariant on T contravariant on U having this on the interface rather than on the type parameters themselves indicates that we're talking about a quality of the interface, not of the type parameter. all other options except #5 (especially #3) seem to lead people to think more about what the type parameter is, when variance actually says nothing about that type parameter, but only about the generic interface's (or delegate's) relation to that parameter. The example above reads quite intuitively. I have an interface with two parameters, they can be whatever the user likes (unless there are additional where constraints). the interface is covariant on T and contravariant on U. covariant means that assignability of IFoo goes with T, and contravariant goes into the other direction. this is not hard to remember, "co" and "contra" express this well. The only thing that needs to be remembered (or logically deduced, which is possible) is that covariance is typically useful for output parameters, while contravariance is typically useful for input parameters. since this is only the typical case, and wrong in others (Meta<+T>) I don't see how you could make this mapping more implicit. My opinion is that for the sake of correctness, we have to accept this difficulty.Anonymous
October 31, 2007
How about re-using the existing c# syntax and allow re-ordering the parameters interface IFoo<T, U> where T: Mammal where Mammal : U { }Anonymous
November 01, 2007
> I still don't understand how *:U expresses "something which U extends". You and Bradley are right, that is badly worded. I intended to say "something which extends U". I'll fix it.Anonymous
November 01, 2007
Stuart: I want to be able to declare a variable of type List<?> Indeed, when we were designing anonymous types we considered doing that kind of inference. We're calling those "mumble types", as in "I have a list of... mumble mumble mumble". Obviously we didn't end up doing them for C# 3.0 but it does seem like a generally useful addition to the type system, so we'll consider it for hypothetical future versions.Anonymous
November 01, 2007
The comment has been removedAnonymous
November 01, 2007
The comment has been removedAnonymous
November 01, 2007
Ooh, or: public delegate R Func<R, A>(A a) where Func<this, A> is Func<base, A> where Func<R, base> is Func<R, this>; Yes, it's verbose, and probably not terribly easy to write correctly (although I suspect the IDE could help a LOT with this kind of clause) but code is read much more often than it's written. And having the code written this way gives some kind of intuitive sense of what it means. Plus it doesn't have the meta problem: public delegate Action<T>(T t) where Action<base> is Action<this>; public delegate Meta<T>(Action<T> action) where Meta<this> is Meta<base>; It accurately expresses what's actually going on there without presuming the meaning as input or output.Anonymous
November 01, 2007
The comment has been removedAnonymous
November 01, 2007
The comment has been removedAnonymous
November 01, 2007
Well, since Stefan already opened the door, I love Spec#, and I want contracts in C# :).Anonymous
November 01, 2007
that's right, mike. DBC would have a greater impact on the way we code than any co/contravariance, however fascinating this is. (even without the amazing static analysis spec# does) (while the door is open, can we please automatically generate unit tests from those contracts too?) but then I want memberof (ldtoken for members), generics and lambdas in attributes, I want Expression<Func<T>> parameterized (lambdas) so that the expression tree does not have to be parsed and processed for every call (I'll just say i4o, the indexed variant of LINQ 2 objects), and ... - no, wait, let's stay focused for a minute, OK? ;-) (ok, I can't hold it back. pattern matching. now I've said it. but before we get into macros and meta-programming, let's get back to that covariance syntax problem of that hypothetical... hey, can we give it an imaginary version number, like C# 4i? or does that sound too much like oracle?)Anonymous
November 01, 2007
interface IFoo<T, U> where T is Animal, Mammal is U looks very intuitive - no need to remember anything.Anonymous
November 02, 2007
In my opinion it is important to have a clear verbose format. This is something quite complicated that should not be hidden away in a small added + or -. I am just brainstorming here, but how about this (introducing a new keyword "assignable to"): interface IFoo<T,U> assignable to IFoo<R, S> where R is T where U is S; { ... } This has the advantage of making it extremely clear for the user of the interface what flexibility the co-/contravariance gives him, at the cost of making it unclear for the implementor which restrictions this imposes on him. Since interface-users should clearly outnumber implementors - especially for the types of interface where co- and contravariance is an issue - this should be an ok tradeoff.Anonymous
November 02, 2007
The comment has been removedAnonymous
November 02, 2007
The comment has been removedAnonymous
November 02, 2007
The comment has been removedAnonymous
November 02, 2007
interface IFoo<T, U, V> is IFoo<base(T), U, derived(V)>Anonymous
November 02, 2007
Another variant on the notation I proposed earlier: delegate R Func<R, A>(A a) where Func<R, base(A)> is Func<base(R), A>; or delegate R Func<R, A>(A a) where Func<R, A> is Func<base(R), A> where Func<R, base(A)> is Func<R, A>;Anonymous
November 02, 2007
stuart: but this re-introduces the double-meaning of where (contraints/variance), plus I don't think that base() and derived() make it any clearer who is the base and who derives than the T:* notation I'm still not entirely happy with the "is" constraint syntax. it's clear, but is it really useful to have a syntax that spells out the meaning of variance? I don't have a problem with the verbosity, but for me the "covariant on" and "contravariant on" clauses on the interface (not on the type arguments) would be the most intuitive ones when I'm working with it. I have to grasp co/contravariance once, the only notation that takes this away is the in/out notation, which is either wrong (Erics option #5) or overly verbose (my proposal of applying the in/out constraints not only to T, but maybe also to IDoer<T> etc.). plus, while in/out syntax is easier to write and understand, it makes it much harder to actually understand what assignment compatibility it really achieves. I also ask everyone to challenge my assumption that automatic variance is less of a problem for delegates than for interfaces. the more I think about it, I believe that it would be a Good Thing, probably even as opt-out (on by default). Am I wrong here? to summarize, for interfaces I'd prefer interface IFoo<T,U> covariant on T contravariant on U clear in what it does (google-able keywords) to which party (interface, not type parameters). once you grasped, easy to understand which assignment results from it (co: same as type-parameter, contra: opposite) for delegates I'd prefer automatic co/contravariance, maybe with the possibility to opt out ("invariant on T") everything else I've posted are mere thoughts that I would not like to see in the language unless somebody comes up with better variations.Anonymous
November 02, 2007
Stefan, I don't need to challenge your proposal of automatic variance on delegates, because Eric already ruled it out as actually impossible. delegate void Circular1<T>(Circular2<T> param); delegate void Circular2<T>(Circular1<T> param); If Circular1 is covariant on T then Circular2 is contravariant on it, and vice versa. So simply stating that variance is intended is not sufficient, you really do have to spell out which way round. I could live with the "covariant on" / "contravariant on" syntax but I actually do think it's helpful to spell out the meaning of it. Even after all eight of Eric's blog posts on the subject, I still didn't really fully grok it until I started thinking about it in terms of how IFoo<T> relates to IFoo<T's base class> and IFoo<T's derived classes>. Describing the meaning concretely, in terms of how it relates to the actual type you're declaring, made a huge difference to my ability to understand it. I can actually write statements about Action and Meta and get their variance the right way round with this kind of syntax. Which isn't true for any of the others, including "covariant on" and "contravariant on" - at least until I spent some time writing code using them. And I don't think that users of the interfaces will ever spend enough time to get that understanding.Anonymous
November 02, 2007
Thank you all for this fascinating discussion. I have not yet absorbed nearly all of it. There is a wealth of possibilities here which I shall summarize and take to the design team at some point over the next few weeks. To answer the earlier question -- the proposed feature is guaranteed-typesafe interface and delegate variance on reference types, no more, no less. No variance on classes, at least not this go-round. No unsafe variance. No call-site variance, no virtual overload return type variance, etc. Those are all features that we will consider, of course, but interface and delegate variance is the one I'm interested in today.Anonymous
November 02, 2007
The comment has been removedAnonymous
November 03, 2007
The comment has been removedAnonymous
November 03, 2007
The comment has been removedAnonymous
November 03, 2007
Thomas, I liked that at the first look. However, I think you have to know what co/contravariance is in order to understand this. Otherwise, it would be misleading. Just reading your syntax and not knowing about variance, I'd assume that this somehow indicates that an IFoo<Mammal> can take an Animal object (for accept base/contravariance), which is nonsense, or a Giraffe object (for accept derived/covariance), which goes without saying.Anonymous
November 04, 2007
Welcome to the thirty-fifth edition of Community Convergence. We have an interesting and controversialAnonymous
November 05, 2007
Stefan's idea of automatic variance for delegates is growing on me. If you automatically assign variance in the cases where it can be unambiguously determined (which is 99% of cases) but also allow it to be specified manually using the same syntax that interfaces use, that seems to fit well with the "simple things should be simple; complex things should be possible" principle. I'm not sure you even need a syntax to explicitly specify that a delegate type should NOT be variant when it could be. You don't want it to be automatic on interfaces because of the fact that adding members to the interface could inadvertantly change the variance - and adding members to an interface is not normally a breaking change for consumers of the interface (although it is for implementers). I'm not sure there's any equivalent to that for delegates - ANY change to a delegate is a breaking change for consumers of the delegate. So if the variance changes too in that case it's not a big deal as all code consuming the delegate is already broken.Anonymous
November 05, 2007
The comment has been removedAnonymous
November 06, 2007
While scanning the examples, something like T:*, *:U really draws my attention to the * in the font my browser uses. I'm wondering if something like T:var, var:U might be better. Does var have the right connotation linking the generic's type to its variance?Anonymous
November 07, 2007
You could combines Lukes idea with the F# way and use underscores instead of asterisks: delegate R Func<_ is A, R is _> (A a)Anonymous
November 08, 2007
hey, if * looks bad in murman's font, shouldn't we make this symbol configurable? ;-)Anonymous
December 06, 2007
Personally, I like #1 and #5. I haven't read the comments... perhaps some much smarter syntax was introduced there.Anonymous
December 19, 2007
"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.Anonymous
October 04, 2008
The comment has been removedAnonymous
October 08, 2008
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.Anonymous
October 08, 2008
delegate R Func<_ is A, R is _> (A a) is good another option: delegate R Func<A, R> (A a) where R>AAnonymous
December 23, 2008
So nicely step by step blogged by Eric Lippert for "Covariance and Contravariance" as "Fabulous