Page 2 of 3
Now let's look at these ideas when applied to functions.
If you define a function which accepts an object of type A as its input parameter and returns an object of type B as its output then, by the substitution principle or just because a derived class B has all of the properties of A, you can use a B as the input and treat the return a B as the result.
That is if you have a function:
B MyFunction(A param)
return new B
you can write
A MyA=MyFunction(new B);
That is the input parameter can be a B and B<A and this leads on to the idea that input parameters are contravariant. The result can be treated as an A and A>B and this leads on to the idea that output parameters i.e. results are covariant.
To see that this is so we have to shift our viewpoint a little.
Let's think about this from the point of view of the function for a moment. Consider two functions that just differ in the type of their input parameters with A>B:
void MyFunction1(A param)
void MyFunction2(B param)
By the substitution principle MyFunction1 can accept a B as its input and so can be used anywhere you use a MyFunction2.
Hence by the reverse substitution principle you have to regard MyFunction1 as derived from MyFunction2 and MyFunction1<MyFunction2.
That is changing the input parameter type from A to B where A>B results in MyFunction1<MyFunction2
You can now see clearly why this is contravariance. Changing the parameter from type A to type B in the function definition where A>B results in MyFunction1<MyFunction2 - the resulting type ordering goes the other way.
Now lets repeat the argument but with two functions that only differ by their output type where A>B:
return new A;
return new B;
In this case the by the substitution principle it is clear that MyFunction2 can be used anywhere MyFunction1 can, because it returns a B which can always be treated as an A, and hence MyFunction2 has to be considered as derived from MyFunction1 i.e. MyFunction1>MyFunction2.
Thus A>B results in MyFunction1>MyFunction2.
You can see that changing the output type from A to B i.e. going towards more derived makes the new function more derived and the change is covariant.
So changing input parameter type to be more derived makes the function less derived and changing the ouput type to be more derived makes the function more derived.
The general principle
Now that you have looked at the way that a change to a function effects its type we can generalise the idea of covariance and contravariance to any situation - not just where functions are involved.
Suppose we have two types A and B and we have a modification, or transformation T, that we can make to both of them to give new types T(A) and T(B).
- If T is a covariant transformation we have A>B implies T(A)>T(B).
- If T is a contravariant transformation then we have A>B implies T(A)<T(B).
- It is also possible that neither relationship applies that is A>B doesn't imply anything about the relationship between T(A) and T(B). In this case T is refered to as invariant.
Our earlier function example can be recast into this form by inventing the transformation T that converts a type into a function with a parameter of that type. As already worked out T is clearly contravariant.
That is, T(A)=function(A) and T(B) = function(B), where function(A) is any function returning any type and accepting a parameter of type A and, as we have demonstrated in the previous section, A>B implies:
In the same way a transformation that converts a type into a function that returns the type is covariant.
That is, T(A)=A function() and T(B)=B function, where A function is any function of any parameter type that returns a type A, and B function is any function of any parameter types that returns a type B, then A>B implies:
T(A)=A function()>B function=T(B)
This is a completly general idea.
For example consider the transformation that converts a type into an array of that type i.e. T(A) is and array of type A or A. If A>B an array of B can be substituted for an array of A and so A>B. Hence forming an array is covariant.