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.