Covariance and Contravariance
Motivation
Along the inheritance chain, it sometimes makes sense to change the type of arguments or results of inherited features along the way to match the actual type. An example is an heir with a feature that is more specialized, so it can provide functions that return a more specialized result but that also may require more specialized arguments.
In sub-typing, the Lizkov substitution principle gives a simple rule in order to guarantee that a sub-type can be used in all cases its super-type can be used: Argument types in a redefinition can only change in a contravariant way (becoming less specific) while result types can change only in a covariant way (becoming more specific).
Unfortunately, relations in the real world often do not respect Lizkov's
principle: An abstract numeric type may provide an add
function
that receives another instance of numeric
as an argument and
produces an instance of numeric
as its result. Sub-types of
numeric
could be i32
or vector f64
. In
these sub-types, it makes sense to use covariance for the argument type as well
as for the result type, i.e., add
on an i32
should
require another i32
and produce an i32
result, while
add
on a vector f64
should require another
vector f64
and produce a vector f64
result. Lizkov's
principle is not respected for the covariant change in argument types.
Covariance using this.type
Using this.type
with corresponding rules can solve the
common case of co-variant argument and result types that are of the type of an
outer feature.
The following code gives an example of a feature joinable
and
three different children that can be joined.
The same example but trying to call join with wrong arguments results in errors:
Covariance using type parameters
Using a type parameter in the super-type that is replaced by a concrete type in the sub-type enables the sub-type to make both co-variant and contra-variant changes to argument and result types. However, all code using the super-type then also has to receive a type parameter to be applicable to concrete sub-types.
The following code implements the example from above using type parameters
instead of this.type
. For this, children have to add themselves as
actual type arguments.