Covariance and Contravariance in C#, Part Ten: Dealing With Ambiguity

Covariance and Contravariance in C#, Part Ten: Dealing With Ambiguity

Rate This
  • Comments 43

OK, I wasn’t quite done. One more variance post!

Smart people: suppose we made IEnumerable<T> covariant in T. What should this program fragment do?

class C : IEnumerable<Giraffe>, IEnumerable<Turtle> {
    IEnumerator<Giraffe> IEnumerable<Giraffe>.GetEnumerator() {
        yield return new Giraffe();
    }
    IEnumerator<Turtle> IEnumerable<Turtle>.GetEnumerator() {
        yield return new Turtle();
    }
// [etc.]
}
 
class Program {
    static void Main()  {
        IEnumerable<Animal> animals = new C();
        Console.WriteLine(animals.First().GetType().ToString());
    }
}

Options:

1) Compile-time error.
2) Run-time error.
3) Always enumerate Giraffes.
4) Always enumerate Turtles.
5) Choose Giraffes vs Turtles at runtime.
6) Other, please specify.

And if you like any options other than (1), should this be a compile-time warning?

  • 1. Compile time error.

    Also,

    object o = new C();

    var e = (IEnumerable<Animal>) o;

    should be a runtime error.

    Some sticky situations are harder, though. For example, is it possible to make this work?

    IEnumerable<Animal> a1 = (IEnumerable<Giraffe>) new C();

    IEnumerable<Animal> a2 = (IEnumerable<Turtle>) new C();

    Console.WriteLine(a1.First().GetType().ToString()); // Giraffe

    Console.WriteLine(a2.First().GetType().ToString()); // Turtle

  • There is no reasonably obvious expected behavior if that code were to compile, so it would have to be a compile time error.

  • My uneducated guess is 3) Always enumerate Giraffes.

    Being as C inherits from IEnumerable<Giraffe> first, this is the less ambiguous option.

  • 1. Compile time error (gut reaction)

    Thinking outside the box, though, I wonder...

    Could we instead use this as an opt-in to aggregation? Something along the lines of

    IEnumerable<Animal> animals = new C();

    // Both signatures qualify, interface list determines order

    foreach(Animal a in animals)

    {

     Console.WriteLine(a.GetType().ToString());

    }

    "Giraffe"

    "Turtle"

    There has to be a reason why that's wrong :-)

  • Absolutely compile-time. Anything else would be super-confusing.

  • In the example above, didn't the programmer mean IEnumberable<Animal>?

    Or, perhaps IEnumerable<T> where T : Giraffe, Turtle?

    But, assuming this is a more legitimate case of two IEnumerable<> types, it seems the compiler should support this. (Not a compile time error.)

    The LHS is requesting (in the example) a very specific interface. The IEnumerable<Turtle> interface. At compile time, it seems plausible that this type could be deduced. In fact, you should be able to follow this up the heirarchy to IEnumerable<Animal> and so on.

    Chuck (crystal_bit@hotmail.com)

  • Good question. Makes me wonder how variance is implemented on vtable level...

    1 - compile time error whenever it can be detected (and runtime error otherwise)

    Additionally, if it were not for backwards compatibility, I'm not sure if it's necessary to allow the implementation of a covariant interface for two Ts that share a common base type. Too confusing, too hard to guess what happens, and prone to cause runtime-errors. Maybe a warning would be a good idea: "CSxxxx - Don't implement covariant interfaces that way unless you _really_ know what you're doing." On the other hand, while only true for the special case of IEnumerable<T>: if you implement IEnumerable<T> for two different Ts, you still have to implement the non-generic IEnumerable. If that doesn't tip you off, you might as well accept the punishment ;-)

    And yes, your blog is fascinating enough that I care to read and comment although your policy of not answering most of my comments has not changed so far. It could be so much better though.

  • I agree with Stuart Ballard:

    The compiler should in no case choose one over the other -> compile time error.

    The more specialiced case would be, what if there's an implicit and an explicit interface implemenation?

    class C : IEnumerable<Giraffe>, IEnumerable<Turtle> {

       IEnumerator<Giraffe> GetEnumerator() {

           yield return new Giraffe();

       }

       IEnumerator<Turtle> IEnumerable<Turtle>.GetEnumerator() {

           yield return new Turtle();

       }

    // [etc.]

    }

    class Program {

       static void Main()  {

           IEnumerable<Animal> animals = new C();

           Console.WriteLine(animals.First().GetType().ToString());

       }

    }

    Should the compiler choose the implicit implementation or also cause a compiler error?

    Another solution comes into my mind:

    What if the compiler would issue an error because the class implements two "overlapping" covariant interfaces? The two covariant interfaces IEnumerable<Giraffe> and IEnumerable<Turtle> should maybe have some kind of equivalence relation in the context of implementing them by a class.

  • I would expect to respond in the same way it responds with any other ambiguity: compile-error.

    This is an interesting scenario.  What you're essentially allowing programmers to implement would be similar to:

    interface IBase

    {

    int Method ( );

    }

    interface IOne : IBase

    {

    }

    interface ITwo : IBase

    {

    }

    class OneClass : IOne, ITwo

    {

    int IOne.Method ( )

    {

    return 1;

    }

    int ITwo.Method ( )

    {

    return 2;

    }

    }

    ...which, of course, is not syntactically correct.

  • Additional question (I guess it's really the same question as the vtable implementation): Is an assignment of an IEnumerable<Giraffe> to an IEnumerable<Animal> variable statically verifyable, i.e. is it just copying a DWORD at runtime, or do you have to check or even convert the assignment?

    Either way, I'd argue that implementing IEnumerable<T> for Giraffe AND Turtle should break covariance. I noted that the CLR currently chooses option 3 (first interface implemented), and I really think this is dangerous. I know I'm asking a lot for breaking changes, this looks wrong. I say discourage these implementations (using warnings), and break covariance if the programmer insists.

  • Exactly the same problem already exists with user-defined conversions:

    class Program

    {

    public static void Main(string[] args)

    {

    Animal a = new SomeClass();

    Console.WriteLine(a.GetType().Name);

    }

    }

    abstract class Animal { }

    class Giraffe : Animal { }

    class Turtle : Animal { }

    class SomeClass

    {

    public static implicit operator Giraffe(SomeClass c)

    {

    return new Giraffe();

    }

    public static implicit operator Turtle(SomeClass c)

    {

    return new Turtle();

    }

    }

    It results in a compile time error (ambiguous user-defined operators SomeClass->Giraffe and SomeClass->Turtle), so that's what should happen for the covariant IEnumerable, too.

  • It can't be compile time error because of the philosophy that adding an interface to a class in a library should never break client code.

    Also you can't choose between giraffes and turtles  neither at runtime or compile time for the very same reason. An library implementer might change the order of interface declarations and not preserve it.

    The only solution I see is to allow only one 'implicit' interface implementation for a covariant interface and the others should be 'explicit'. Why? Because both IEnumerable<Giraffe> and IEnumerable<Turtle> are the same as IEnumerable<Animal> when T is covariant, basically implementing IEnumerable<Animal> twice,  with the exception that they carry extra type info that can be used to skip casts.

    {

    Other solution might be not to mark IEnumerable<T+>  as covariant but to mark it in the implemented class

    class C : IEnumerable<Giraffe+>, IEnumerable<Turtle>  

    where IEnumerable<Giraffe+> is now covariant? (uh those terms make my head spin)

    }

  • Definitely 1 (compile-time error). Please avoid confusion whenever possible. We don't want programming to become gambling, do we?

  • It should be a compile time error. Because when you consider code like

    IEnumerable<Animal> animals = c;

    multiple paths exist for the conversion to succeed. Compiler should die the death of Buridan's ass, since there is no motivation to choose one conversion over the other.

    This problem readily manifests itself in C++ where multiply inheriting a class is allowed, the famous "diamond" hierarchies of inheritance. In CLR, I think it is the only closest feature to diamond hierarchies.

  • Without further language enhancements, in the face of non(-obvious)-determinism, I say...

    Compile time error.

    Except it is possible to image a scenario where such a setup would be useful.  Consider the enumerable to be a producer of some sort of "Item"s.  As a handy feature, it can directly produce the appropriately subclassed "SmallItem" or "ScalableItem".  Such a producer might implement IEnumerable<SmallItem> and IEnumerable<ScalableItem>  such that that the consumer can get the appropriately formed items he needs easily.

    Currently, a software doing something like that would do so precisely to do a poor-mans covariance, and is trying to be useful while at it.  A feature which saves the consumer effort today, (by allowing the IEnumerable client to just pick what's needed), would turn into a confusing situation for in this version - why can I do covariance casting on "almost" all interface implementations, but not this one?

    This situation, in essence, already exists today, and the solution there is the right one.

    If you want to implement IEnumerable<a> and IEnumerable<b>, what do you do in a foreach?  You iterate over the IEnumerable - i.e. you force the interface implementor to choose.

    Ideally, if you implement two interfaces which are potentially covariant, you should at the least be forced to prioritize (in some fashion), and at worst to implement the border case explicitly (nasty, not preferred).  Of course, that means that in a hypothetical future CLR marking IEnumerable<T> covariant would be a (massively) breaking change...  that's why I'm saying ideally, since that's probably not reasonable.

Page 1 of 3 (43 items) 123