Fuzion Logo
fuzion-lang.dev — The Fuzion Language Portal
JavaScript seems to be disabled. Functionality is limited.

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.