Type Bounds

When we express a subtype-supertype relationship by writing T <: U, then we say that T is a subtype by U. We can use this relationship between two types in the instantiation of generic types in functions.

Type bounds in functions

Consider the following function signature:

func makeNat<T <: Number>(x : T) : Natural

It's a generic function that specifies a type bound Number for its generic type parameter T. This is expressed as <T <: Number>.

The function takes an argument of generic bounded type T and returns a value of type Natural. The function is meant to take a general kind of number and process it into a Nat.

The types Number and Natural are declared like this:

type Natural = {
    #N : Nat;
};

type Integer = {
    #I : Int;
};

type Floating = {
    #F : Float;
};

type Number = Natural or Integer or Floating;

The types Natural, Integer and Floating are just variants with one field and associated types Nat, Int and Float respectively.

The Number type is a type union of Natural, Integer and Floating. A type union is constructed using the or keyword. This means that a Number could be either a Natural, an Integer or a Floating.

We would use these types to implement our function like this:

import Int "mo:base/Int";
import Float "mo:base/Float";

func makeNat<T <: Number>(x : T) : Natural {
    switch (x) {
        case (#N n) {
            #N n;
        };
        case (#I i) {
            #N(Int.abs(i));
        };
        case (#F f) {
            let rounded = Float.nearest(f);
            let integer = Float.toInt(rounded);
            let natural = Int.abs(integer);
            #N natural;
        };
    };
};

After importing the Int and Float modules from the Base Library, we declare our function and implement a switch expression for the argument x.

In case we find a #N we know we are dealing with a Natural and thus immediately return the the same variant and associated value that we refer to as n.

In case we find an #I we know we are dealing with an Integer and thus take the associated value i and apply the abs() function from the Int module to turn the Int into a Nat. We return a value of type Natural once again.

In case we find a #F we know we are dealing with a Floating. So we take the associated value f of type Float, round it off and convert it to an Int using functions from the Float module and convert to a Nat again to return a value of type Natural once again.

Lets test our function using some assertions:

assert makeNat(#N 0) == #N 0;

assert makeNat(#I(-10)) == #N 10;

assert makeNat(#F(-5.9)) == #N 6;

We use arguments of type Natural, Integer and Floating with associated types Nat, Int and Float respectively. They all are accepted by our function.

In all three cases, we get back a value of type Natural with an associated value of type Nat.

The Any and None types

All types in Motoko are bounded by a special type, namely the Any type. This type is the supertype of all types and thus all types are a subtype of the Any type. We may refer to it as the top type. Any value or expression in Motoko can be of type Any.

Another special type in Motoko is the None type. This type is the subtype of all types and thus all types are a supertype of None. We may refer to it as the bottom type. No value in Motoko can have the None type, but some expressions can.

NOTE
Even though no value has type None, it is still useful for typing expressions that don't produce a value, such as infinite loops, early exits via return and throw and other constructs that divert control-flow (like Debug.trap : Text -> None)

For any type T in Motoko, the following subtype-supertype relationship holds:

None <: T

T <: Any