Skip to main content

Advanced types

Advanced type features enable more flexible and expressive type definitions, including structural equality, generic types, subtyping, recursive types, and type bounds.

Structural equality

Structural equality determines whether two values are equal based on their contents. This applies to immutable data structures, such as records and variants, but does not apply to mutable structures for safety reasons.

type Point = { x : Int; y : Int };

let p1 : Point = { x = 1; y = 2 };
let p2 : Point = { x = 1; y = 2 };

p1 == p2; // true (structural equality)

Even though p1 and p2 are distinct objects, they are considered equal because they have the same structure and values.

This remains true even if different fields are added to the point values, since the == on Point values only considers the x and y fields and ignores other fields.

type Point = { x : Int; y : Int };

let p1 : Point = { x = 1; y = 2; z = 3 };
let p2 : Point = { x = 1; y = 2; z = 4; c = "Red"; };

p1 == p2; // true (structural equality at type `Point`)

Generic types

Generic types are used to define type parameters that work with multiple data types, commonly used in functions, classes, and data structures.

// Generic function
func identity<T>(x : T) : T {
return x;
};

identity<Nat>(42); // num is Nat

A generic class can store any type while maintaining type safety:

class Box<T>(value : T) {
public func open() : T { value };
};

let intBox = Box<Nat>(10);
intBox.open();

Recursive types

Recursive types allow a type to refer to itself, enabling the creation of nested structures while maintaining type safety. The base package utilizes recursive types to define linked lists.

type List = ?(Nat, List);

This defines a recursive type for representing a linked list of natural number. Each list is either:

  • null, representing the empty list.

  • ?(head, tail), where head is a Nat and tail is another List.

?(1, ?(2, ?(3, null)))  // A list: 1 → 2 → 3

To generalize this structure and support values of any type, we introduce a parameterized type:

type List<T> = ?(T, List<T>);

This defines a generic linked list, where T can be any type (Nat, Text, Blob, or a custom type).

Manually reversing a linked list

Reversing a linked list involves iterating through the list and prepending each element to a new list. This approach demonstrates list traversal without using List.reverse library function.

Non-parameterized type:

// Lists of naturals
type List = ?(Nat, List);

// Reverses List
func reverseNat(l : List) : List {
var current = l;
var rev : List = null;

loop {
switch (current) {
case (?(head, tail)) {
rev := ?(head, rev);
current := tail;
};
case (null) {
return rev;
};
};
};
};
let numbers : List = ?(1, ?(2, ?(3, null)));
reverseNat(numbers); // ?(3, ?(2, ?(1, null)))

Parameterized:

// Lists of naturals
type List<T> = ?(T, List<T>);

// Reverses List
func reverse<T>(l : List<T>) : List<T> {
var current = l;
var rev : List<T> = null;
loop {
switch (current) {
case (?(head, tail)) {
rev := ?(head, rev);
current := tail;
};
case (null) {
return rev;
};
};
};
};

These type and function definitions generalize the previous code to work not just on lists of Nats, but on lists of T values, for any type T.

You can reverse a list of numbers.

let numbers : List<Nat> = ?(1, ?(2, ?(3, null)));
reverse<Nat>(numbers); // ?(3, ?(2, ?(1, null)))

But you can also reverse a list of characters:


let chars : List<Char> = ?('a', ?('b', ?('c', null)));
reverse<Char>(numbers); // ?('c', ?('b', ?('a', null)))

Notice how generic types and generic functions complement each other.

Type bounds

Generic types can use subtype constraints, ensuring that any type used in a generic function meets specific structural or concrete type requirements.

These constraints are enforced at compilation. This guarantees that the necessary properties or operations are available when the function is used, eliminating certain classes of runtime errors.

Although the concept of type bounds is often associated with inheritance-based polymorphism in other languages, Motoko uses structural typing. This means that the subtype relationship is determined by the structure of the types rather than an explicit inheritance hierarchy. Motoko does not support inheritance.

This approach balances the flexibility of generic programming with the safety of compile-time checks, enabling the creation of generic functions that operate on a range of types while still enforcing specific structural or type constraints.

The following examples illustrate this behavior:

func printName<T <: { name : Text }>(x : T): Text {
debug_show(x.name);
};

let ghost = { name = "Motoko"; age = 30 };
printName(ghost); // Allowed since 'ghost' has a 'name' field.

In the example above, T <: { name : Text } requires that any type used for T must be a subtype of the record { name : Text }, that is, it must have at least a name field of type Text. Extra fields are permitted, but the name field is mandatory.

Type bounds are not limited to records. In general, the notation T <: A in a parameter declaration mandates that any type provided for type parameter T must be a subtype of the specified type A. For example, it is possible to constrain a generic type to be a subtype of a primitive type.

func max<T <: Int>(x : T, y : T) : T {
if (x <= y) y else x
};
max<Int>(-5, -10); // returns -5 : Int

Here, T <: Int constrains T to be a subtype of Int, ensuring that arithmetic operations are valid.

But the function can also be used to return the maximum of two Nats and still produce a Nat (not an Int).

max<Nat>(5, 10); // returns 10 : Nat

Actor reference expression

The actor reference expression actor <exp> compute a reference to an actor from a text argument <exp>, the textual encoding of a canister id. The expression is typically combined with an (actor) type annotation, actor <exp> : <typ> that declares the expected type of the actor reference. The argument can either be a text literal, identifier, or, when enclosed in parentheses, a more complicated expression that computes a textual id.

A simple example of using actor references is to access the management canister with textual address "aaaaa-aa". Amongst other things, it has a method raw_rand for generating cryptographically random bytes as a Blob.

persistent actor Coin {
public func flip() : async Bool {
let managementCanister = actor "aaaaa-aa" : actor { raw_rand : () -> async Blob };
let entropy = await managementCanister.raw_rand();
(entropy[0] & 1) == 1;
};
}

A variation computes the textual canister identifier from a given principal. A call to flipWith(p) will succeed is called with Principal.fromBlob("aaaaa-aa"), but may fail with another argument, if the canister does not exist or does not have a raw_rand function:

import Principal "mo:base/Principal";

persistent actor Coin {
public func flipWith(principal : Principal) : async Bool {
let canister = actor (Principal.toText(principal)) : actor { raw_rand : () -> async Blob };
let entropy = await canister.raw_rand();
(entropy[0] & 1) == 1;
};
}

:::warn

There is currently no way to verify the actual type of the actor is compatible with the declared type. The validity of the actor reference and any type incompatibility will be detected later, on interaction with the actor. For this reason, you should only use actor references sparingly. It's typically safer to use explicitly imported canisters with their given actor types and avoid using Text or Principal to represent canisters in your methods (just use actor types instead).

For example, a safer variant of flipWith is:

persistent actor Coin {
public func flipWith(canister : actor { raw_rand : () -> async Blob }) : async Bool {
let entropy = await canister.raw_rand();
(entropy[0] & 1) == 1;
};
}

This flipWith takes a strongly-typed actor, not a weakly-typed Principal.

Actor type annotations offer flexibility when working with external canisters, but there’s no guarantee that the function signatures will match at runtime. If the signatures don’t align, the calls will fail.

As another example of using actor reference expressions, we present a simple Publisher actor that tracks sets of subscribers by their Principal and uses an actor reference expression access the notify of each subscriber when a message is published:

import Array "mo:base/Array";
import Principal "mo:base/Principal";

actor Publisher {
stable var subscribers : [Principal] = [];

public shared func subscribe(subscriber : Principal) : async () {
if (Array.find<Principal>(subscribers, func(s) { s == subscriber }) == null) {
let newSubscribers = Array.tabulate<Principal>(
subscribers.size() + 1,
func(i) { if (i < subscribers.size()) subscribers[i] else subscriber }
);
subscribers := newSubscribers;
};
};

public shared func publish(message : Text) : async () {
for (sub in subscribers.vals()) {
let subActor = actor(Principal.toText(sub)) : actor { notify : (Text) -> async () };
await subActor.notify(message);
};
};
};