Variants
Variant type describe values that take on one of several forms, each labeled with a distinct tag. Unlike records, where all fields exist at once, a value of a variant type holds exactly one of the type's possible values. This makes variants useful for representing mutually exclusive alternatives such as states, enumerations, categories and even trees.
Defining a variant
type Status = {
#Active;
#Inactive;
#Banned : Text;
};
#Active
and #Inactive
are constant tags, with an implicit ()
argument, meaning they only store trivial data. #Banned
carries a Text
value, such as the reason for the ban.
Assigning variants
To assign a variant value, use one of the defined tags.
let activeUser = #Active;
let bannedUser = #Banned("Violation of rules");
Accessing a variant's value
To work with a variant, use a switch
expression to match each possible case.
import Debug "mo:base/Debug";
let activeUser : Status = #Active;
let bannedUser : Status = #Banned("Violation of rules");
func getStatusMessage(status : Status) : Text {
switch (status) {
case (#Active) "User is active";
case (#Inactive) "User is inactive";
case (#Banned(reason)) "User is banned: " # reason;
};
};
Debug.print(getStatusMessage(activeUser));
Debug.print(getStatusMessage(bannedUser));
Variants example: traffic lights
To demonstrate variants, consider the following example.
A traffic light cycles between three distinct states:
- Red: Vehicles must stop.
- Yellow: Vehicles should prepare to stop.
- Green: Vehicles may proceed.
Since the traffic light can only be in one of these states at a time, a variant is well-suited to model it. There is no invalid state, as every possible value is explicitly defined. The transitions are controlled and predictable.
Defining the traffic light state
type TrafficLight = {
#red;
#yellow;
#green;
};
Transitioning between states
A function can define how the traffic light cycles from one state to the next.
func nextState(light : TrafficLight) : TrafficLight {
switch (light) {
case (#red) #green;
case (#green) #yellow;
case (#yellow) #red;
}
};
nextState(#red);
Simulating traffic light changes
import Debug "mo:base/Debug";
import Iter "mo:base/Iter";
func nextState(light : TrafficLight) : TrafficLight {
switch (light) {
case (#red) #green;
case (#green) #yellow;
case (#yellow) #red
}
};
var light : TrafficLight = #red; // Initial state
for (_ in Iter.range(0, 5)) {
// Cycle through states
light := nextState(light);
Debug.print(debug_show (light))
};
Defining a binary tree type using variants
A binary tree is a data structure where each node has up to two child nodes. A variant can be used to represent this structure since a node can either contain a value with left and right children or be an empty leaf. This tree type is recursive as it refers to itself in its definition.
type Tree = {
#node : {
value : Nat;
left : Tree;
right : Tree
};
#leaf
};
This example contains two variants:
#node
contains a value of typeNat
and two child trees (left
andright
).#leaf
represents an empty node.
Building the tree
The following example defines a tree with a single root node containing the value 10
. It has two child nodes, 5
and 15
, both of which do not have any children.
let tree : Tree = #node {
value = 10;
left = #node {value = 5; left = #leaf; right = #leaf};
right = #node {value = 15; left = #leaf; right = #leaf}
};
Tree structure
10
/ \
5 15
Traversing the tree
A tree can be traversed in multiple ways. One common approach is in-order traversal, where nodes are visited in the order:
- Left subtree
- Root node
- Right subtree
The following example recursively traverses the tree in order and prints each value as it is visited.
import Debug "mo:base/Debug";
let tree : Tree = #node {
value = 10;
left = #node {value = 5; left = #leaf; right = #leaf};
right = #node {value = 15; left = #leaf; right = #leaf}
};
func traverseInOrder(t : Tree) {
switch (t) {
case (#leaf) {};
case (#node {value; left; right}) {
traverseInOrder(left);
Debug.print(debug_show (value));
traverseInOrder(right)
}
}
};
traverseInOrder(tree);
Using generic types
Currently, the example tree only supports Nat
values. To allow it to store any type of data, a generic type can be used. A generic type allows a data structure to work with multiple types by using a placeholder type T
, which is replaced with a specific type when used.
type Tree<T> = {
#node : {
value : T;
left : Tree<T>;
right : Tree<T>;
};
#leaf;
};
With this change, the tree can store any type, such as Text
, Nat
, or custom types, making it more flexible and reusable.
Subtyping
In Motoko, a variant with fewer tags is a subtype of a variant with more tags:
type WorkDay = { #mon; #tues; #wed; #thurs; #fri };
type Day = { #sun; #mon; #tues; #wed; #thurs; #fri; #sat};
This means that every WordDay
is also a Day
and, for example, a function on Day
can also be applied to any WorkDay
.