"In a general sense, covariance is achieved when the direction ofassignment compatibility is preserved. Contravariance is achieved whenthe direction of assignment compatibility is reversed."
You omitted an important part there. What Eric Lippert was saying, applied to this context, is that, given some set of types (like your Animal
, Bear
, Camel
) that have assignment compatibility relationships already defined among them, when you use them to construct new kinds of corresponding types (like your Stack<Animal>
, Stack<Bear>
, IPoppable<Animal>
, etc.), those new types may or may not preserve the compatibility at their level (or they might reverse it).
But let's back up a bit. Think about what happens when you use these types polymorphically.
For example, if you create a stack of Bear
-s, that stack can only contain instances of Bear
or of its derivatives, if any.
var bearStack = new Stack<Bear>();bearStack.Push(new Bear("some generic bear"));bearStack.Push(new GrizzlyBear());bearStack.Push(new BlackBear());// Pop returns an instance typed as a Bear (actual type could be a derived class)Bear bear = bearStack.Pop();
Now, can you assign this particular stack to a variable that has the type Stack<Animal>
?
Stack<Animal> animalStack = bearStack;
It feels like you should be able to, but taking polymorphism into account, here's how you can run into problems.
(1) Any Stack<Animal>
should allow you to push any kind of animal to it. That's the contract - that's what the type Stack<Animal>
means, it's what it promises to deliver. That's what you expect when you see that type anywhere in code. However, remember, animalStack
is actually a bearStack
, and it doesn't accept anything other than bears.
animalStack.Push(new Camel()); // just doesn't work!
(2) However, with Pop()
, it's all good. Any client code that needs an Animal
will be perfectly happy to accept a Bear
. A bear is-an animal, so a Bear
instance can be assigned to an Animal
variable.
Animal someAnimal = animalStack.Pop(); // remember, actually bearStack
Or maybe you use Stack<Animal>
as a function parameter:
// This would work with Animal-typed instances, if the language allowed itvoid ReleaseToWilderness(Stack<Animal> animalStack) { while (!animalStack.IsEmpty()) { Animal animal = animalStack.Pop(); animal.MakeHappyNoises(); //... }}// It'd work because it would be safe to call it with a bearStackReleaseToWilderness(bearStack); // ^ here, bearStack is being assigned to the animalStack parameter
However, the language requires you to constrain the interface:
void ReleaseToWilderness(IPoppable<Animal> animalStack) { /* ... */ }ReleaseToWilderness(bearStack); // now this works
The assignment direction that is being preserved here is from more concrete to the more abstract; the direction is the same for both the "base" types and "augmented" types.
Bear ------------ can be assigned to -----> AnimalStack<Bear> ----- can be assigned to -----> IPoppable<Animal>
That is, the stack versions of the types are covariant with respect to the assignment compatibility of the original types.
(3) But notice also that, when it comes to the Push
operation, anyAnimal
stack can be passed to anything that only wants to do pushes to an object that is, as far as that other code is concerned, a push-only Bear
stack:
void AddSomeBears(IPushable<Bear> aThingThatAcceptsBears) { aThingThatAcceptsBears.Push(new Bear()); aThingThatAcceptsBears.Push(new GrizzlyBear()); aThingThatAcceptsBears.Push(new BlackBear()); //...}void AddSomeCamels(IPushable<Camel> aThingThatAcceptsCamels) { //...}var animalStack = new Stack<Animal>(); // NOTE: it's the real Animal stack nowAddSomeBears(animalStack);AddSomeCamels(animalStack);
So, between the "base" types and "augmented" types, the assignment direction is reversed:
Bear ----- can be assigned to -----> AnimalIPushable<Bear> <---- can be assigned from ---- Stack<Animal>
In other words, the stack versions of the types are contravariant with respect to the assignment compatibility of the original types.
More precisely
Bear ------ can be assigned to -----> AnimalIPoppable<Bear> ----- can be assigned to -----> IPoppable<Animal>
A "thing that provides bears" is-also-a "thing that provides animals" (they just happen to all be bears, but that's fine because a bear is-an animal).
And
Bear ----- can be assigned to -----> AnimalIPushable<Bear> <---- can be assigned from ---- IPushable<Animal>
A "thing that accepts any animal" is-also-a "thing that accepts bears" - but this is not true the other way around.
A Stack<Bear>
instance can only be substituted for Stack<Animal>
if the client code that uses the Stack<Animal>
limits itself to only use the parts of the Stack<Animal>
interface that allow for covariance - that is, outputs are fine, but inputs are not because they could be incompatible (can't push anything that's not a bear).
Similarly, a Stack<Animal>
instance can be substituted for a Stack<Bear>
given that the client code limits itself to only use the parts of the Stack<Bear>
interface that allow for contravariance - providing bears as input is fine, but asking for a bear is not, cause you might get a camel, or a cat, or something.
As an aside: You can also see this as Liskov Substitution Principle in action (it's one manifestation of it).
In the LSP sense, by Liskov-Wing definition of subtype, IPoppable<Bear>
(including Stack<Bear>
) can be conceptually seen as a subtype of IPoppable<Animal>
, while IPushable<Animal>
(including Stack<Animal>
) can be seen as a subtype of IPushable<Bear>
(or IPushable<Camel>
, or...).