Covariance and Contravariance in C#, Part Six: Interface Variance

Covariance and Contravariance in C#, Part Six: Interface Variance

Rate This
  • Comments 12

Over the last few posts I’ve discussed how it is possible to treat a delegate as contravariant in its arguments and covariant in its return type. A delegate is basically just an object which represents a single function call; we can do this same kind of thing to other things which represent function calls. Interfaces, for example, are contracts which specify what set of function calls are available on a particular object.

This means that we could extend the notion of variance to interface definitions as well, using the same rules as we had for delegates. For example, consider

public interface IEnumerator<T> : IDisposable, IEnumerator {
  new T Current { get; }
}

Here we have a generic interface where the sole use of the parameter is in an output position. We could therefore make the parameter covariant. That would mean that it would be legal to assign an object implementing IEnumerator<Giraffe> to a variable of type IEnumerator<Animal>. Since the user of that variable will always expect an Animal to come out, and the actual backing implementation always produces a Giraffe, everyone is happy.

Once we’ve got IEnumerator<+T>, we can then notice that IEnumerable<T> is defined as:

public interface IEnumerable<T> : IEnumerable {
  new IEnumerator<T> GetEnumerator();
}

Again, the parameter appears only in an output position, so we could make IEnumerable<+T> covariant as well.

This then opens up a whole slew of nice scenarios. Today, this code would fail to compile:

void FeedAnimals(IEnumerable<Animal> animals) {
  foreach(Animal animal in animals)
    if (animal.Hungry)
      Feed(animal);
}
...
IEnumerable<Giraffe> adultGiraffes = from g in giraffes where g.Age > 5 select g;
FeedAnimals(adultGiraffes);

Because adultGiraffes implements IEnumerable<Giraffe>, not IEnumerable<Animal>. With C# 3.0 you’d have to do a silly and expensive casting operation to make this compile, something like:

FeedAnimals(adultGiraffes.Cast<Animal>());

or

FeedAnimals(from g in adultGiraffes select (Animal)g);

Or whatever. This explicit typing should not be necessary. Unlike arrays (which are read-write) it is perfectly typesafe to treat a read-only list of giraffes as a list of animals.

Similarly, we could make

public interface IComparer<-T> {
  int Compare(T x, T y);
}

into a contravariant interface, since the type parameter is used only in input positions. You could then implement an object which compares two Animals and use it in a context where you need an object which compares two Giraffes without worrying about type system problems.

Next time: Suppose we were to do interface and delegate variance in a hypothetical future version of C#. What would the syntax look like? Is this goofy plus and minus really the best we can do? Do we need any syntax at all?

  • Would the behavior surrounding variant types change based on the +/- modifier? Or does that just make it more explicit to the reader?

  • The former. As I will discuss in my next post,  we need a way to say "this is covariant, this is invariant", etc. We will not be able to deduce the desired variance just from the interface definition, so the default will still be "invariant".

    Uh, hypothetically, of course.

  • Is it a binary behavior, i.e. could we collapse it to a single token specifying "this is variant in the only way that makes sense here"? Hypothetically.

  • An interesting idea -- however, as we'll see in my next post, the variance of one type parameter may depend upon the variance of another. There often is not one single "only way it makes sense here". There are often multiple ways that it could work, and we'd then have to choose.

  • I think one has to grasp the difference between covariance and contravariance to make any good use of it.

    Guessing variance from the interface would be dangerous even if you could do it, because the interface might change later, and this should not hurt existing code by removing or changing variance. The developer of the interface has to make it clear that he wants variance, so he will also be aware that certain changes might not be possible later (or would be breaking).

    During this blog series, I got used pretty well to the +/- notation, so why not. On the other hand, something more verbose like IEnumerable<covariant T> would not hurt either and be more c#-like. It's in declarations only anway.

    Would this, hypothetically, be for interfaces only, or can we have it for generic reference types too?

    This would be especially great because it would put an end to the "no common base type" problem of generics (well, not for value types), so instead of doing perverse reflection calls, we could just rely on

    object x = new List<string>();

    Assert (x is IEnumerable<object>)

    (can't think of an example with classes right now, but I know I wanted it on several occations in the past)

    (one could argue that another way to approach this problem would be to clean up the mess that generics made of the reflection API. probably not easy, but definately due!)

    can't wait. what's the hypothetical timeline? ;-)

  • Would it be wrong to just avoid the covariant and contravariant checks?  With the complications you are showing, I think it is obvious that the parameter in a parametric type is orthogonal to the types of objects managed by that type.

    The illusion of contravariant only appears when you ASSUME the domain you assign a method is the ONLY domain the method can act on.  This assumption does not happen in all languages; a Javascript function can be designed to take a String, but can be passed any non-String.  The output of such a method is well defined: it outputs an exception.  With exceptions being accepted as part of a method's codomain, and realizing that all methods are well defined for all objects, then the issue of "contravariant on input parameters" disappears because all methods act on the same domain (everything).

    I believe little is lost in terms of optimization.  

    Object[] o=new Object[];

    String[] s=o; //Allowed at compile time

    Assigning arrays, or any collection, can be checked at runtime when the contravariant relationship is broken.  But this can be done at the collection level, not at the individual element level, as long as there is no type-erasure.  

    Same for function delegates; checks can be done at runtime.  But I see no reason why simple constant propagation (constant TYPE propagation) would catch most illegal maneuvers at compile time anyway.

    Furthermore, method inheritance must be contravariant on the parameter types to be logically consistent in an OO framework.  And this works by delegating out-of-domain parameters back to the super class.  I believe the logic in such a proof can be mimicked with some kind of recursive function delegate; proving "contravariant on parameter types" is logically inconsistent in a function delegate framework.  But that is only a strong feeling.

  • Yeah, why bother with variance, haven't they heard of duck typing yet? Come on ... ;-)

  • I would love to have this kind of variance support.

    All too often I need to drop down to the IL level to do this kind of work.

    Thus, I am accustomed to the +- notation.

    However, perhaps your next post will have compelling reasons for another notation.

  • Stefan Wenig, I am not suggesting duck typing, I am suggesting strong dynamic typing of type parameters.

    Correction:  " Furthermore, method inheritance must be COVARIANT on the parameter types to be logically consistent in an OO framework."

  • Kyle,

    in this case I don't understand what you're proposing. From your reference to JavaScript I just assumed you were talking about duck typing, which is the only way I know that can do this trick. If you're thinking about something else, is this anything that exists anywhere yet? Mind to post a link? Or is it a new idea you came up with? In this case, I'd ask you to elaborate.

    Which types should be compatible at compile time? Base<anything> and Base<anything else>? Or even Base<anything> and Derived<anything else>? Or maybe even Sibling1<anything> and Sibling2<anything ese>, given they both derive from Base<T>?

    How exactly does this translate to delegates? What do you mean by " Furthermore, method inheritance must be COVARIANT on the parameter types to be logically consistent in an OO framework."? The this parameter? any parameter? why?

    Which kind of checks would have to be made at which point? Would you expect this to work for value type parameters too?

    Sounds like this would hurt my brain even more than explicit co/contravariance, but maybe I'm just not getting it.

    Stefan

  • To go back to a remark in one of the first posts:

    What we strongly lack is overriding a read only property by a more specialized one.

    More generically it would be convenient to define an overload of a method and to have some "OverrideAttribute(typeof(string))" to say e.g.  string Compute(object o) is ovverriding the base class implementation of object Compute(string s).

    In C# it could be expressed as the 'generic keyword' "override<string>".

    Covariance on return types woul be a gift, and covariance of IEnumerable<T> would definitely ease our lifes.

    After reading the CLR version 2 spec, and seeing covariance was not handled in Framework 2.0, I hoped it will be in 3.0. Then in 3.5. Try to make C# at least aware of what is specified in a delegate or interface, even if there is no language support to create it.

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

Page 1 of 1 (12 items)