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.
last changed: 2024-06-28