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
Section titled “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
Section titled “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
Section titled “Accessing a variant’s value”To work with a variant, use a switch expression to match each possible case.
import Debug "mo:core/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
Section titled “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
Section titled “Defining the traffic light state”type TrafficLight = { #red; #yellow; #green;};Transitioning between states
Section titled “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
Section titled “Simulating traffic light changes”import Debug "mo:core/Debug";import Iter "mo:core/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
Section titled “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:
#nodecontains a value of typeNatand two child trees (leftandright).#leafrepresents an empty node.
Building the tree
Section titled “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
Section titled “Tree structure” 10 / \ 5 15Traversing the tree
Section titled “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:core/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
Section titled “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
Section titled “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.