Options
Type | Syntax | Purpose | Application |
---|---|---|---|
Option | ?T | Represents a value that may be missing. | When an operation might return no result. |
Options provide a structured way to represent values that may or may not be present. Instead of using null
directly, the option type enforces explicit handling, making programs safer and reducing unexpected failures.
An option is defined using ?
followed by the type of the value it can hold.
var username : ?Text = null;
username;
username
is an optional Text
value that starts as null
(no username set).
The constant null
is the sole value of Motoko’s trivial Null
type. It also represents the absence of a value in an optional type (?T
). In this role, null
is similar to None
in Rust and Scala, and to Nothing
in Haskell. Likewise, Motoko’s optional type ?T
is conceptually similar to Option<T>
in Rust, Option[T]
in Scala, and Maybe T
in Haskell. In all these languages, the type system enforces explicit handling of missing values by representing optionality through a dedicated type.
When a Motoko value has type ?T
, it is either null
or contains a value, written as ?value
(note the leading ?
). The fundamental way to access this value is by using a switch
expression, which explicitly handles both cases:
func displayName(option : ?Text) : Text {
switch option {
case (?user) { user };
case null { "Guest" };
}
};
displayName(username);
Sometimes, the verbosity of a switch
expression can make code harder to read. To improve readability, Motoko provides additional constructs for working with option types.
Checking for presence
To determine if an option contains a value, function Option.isSome
returns true
if its argument is not null
.
import Option "mo:base/Option";
import Debug "mo:base/Debug";
let value : ?Nat = ?5;
if (Option.isSome(value)) {
Debug.print("Value is present.");
}
By leveraging the Option
module, handling optional values becomes more concise and expressive, reducing the need for explicit switch
statements.
Providing default values
Instead of manually handling null
cases with pattern matching, Option.get
allows for cleaner fallback logic to ensure that missing values are safely replaced with a default.
import Option "mo:base/Option";
Option.get(username, "Guest"); // "Guest" if username is null
Using options for error handling
Options can be used to catch expected failures instead of calling a trap
, making a function return null
when it encounters an invalid input.
func safeDivide(a : Int, b : Int) : ?Int {
if (b == 0) null else ?(a / b);
};
let result1 = safeDivide(10, 2); // ?5
safeDivide(10, 0); // null
Another way to extract values from option types is by using the let ... else
pattern. This approach can be preferable to a switch
expression for brevity and clarity. However, it only applies when the else
branch can redirect control flow, such as returning early or throwing an error, if the value does not match the pattern in the let
.
For example, here’s a simple implementation of an option helper:
func get<T>(option : ?T, defaultValue : T) : T {
let ?value = option else return defaultValue;
return value;
};
assert get(?"A", "B") == "A";
assert get(null, "B") == "B";
The let
statement matches the option against the pattern ?value
, extracting the contained value if present. If the option is null
, the pattern fails to match and the else
branch is executed, typically to exit the function early or return a default value.
The same logic can be expressed using a switch
, though the result is more verbose and introduces an additional level of nesting:
func get<T>(option : ?T, defaultValue : T) : T {
switch option {
case null defaultValue;
case (?value) value;
}
};
Although convenient for option patterns, let-else
also works with other types of patterns.
Applying transformations to options
The Option.map
function applies a transformation only if the value is present.
import Option "mo:base/Option";
let number : ?Nat = ?10;
Option.map<Nat, Nat>(number, func(x : Nat) = x * 2); // ?20
In this example, if number
is null
, map
ensures the result remains null
instead of performing an invalid operation.
Applying an optional function
Sometimes, both the function and value are optional. Option.apply
calls a function only if both are present. This is useful when chaining optional operations that may return null
.
import Option "mo:base/Option";
let maybeIncrement : ?(Nat -> Nat) = ?(func x: Nat = x + 1);
let maybeValue : ?Nat = ?10;
Option.apply<Nat, Nat>(maybeValue, maybeIncrement); // ?11
If either maybeFunction
or maybeValue
is null
, the result remains null
.
Combining multiple optional values
When working with multiple optional values, using Option.chain
processes them safely without unnecessary switch
statements.
import Option "mo:base/Option";
let firstName : ?Text = ?"Motoko";
let lastName : ?Text = ?"Ghost";
func combineNames(f : Text, l : Text) : Text {
f # " " # l;
};
Option.chain<Text, Text>(firstName, func (first : Text) {
Option.map<Text, Text>(lastName, func(last : Text) { combineNames(first, last) });
});
// ?("Motoko Ghost")
Option blocks (do ?
)
Option blocks in Motoko use the syntax do ? <block>
to work with optional values of type ?T
without requiring nested switch
statements. When the <block>
evaluates to a value of type T
, the entire do ?
expression returns a value of type ?T
. Crucially, it allows early exits from the block when encountering null
.
Within a do ? <block>
, the !
operator is used to unwrap values of unrelated option types (e.g., ?U
). When evaluating an expression <exp> !
, if <exp>
results in null
, control immediately exits the do ?
block with value null
. Otherwise, it unwraps the value ?v
and continues with v
of type U
.
The do ? <block>
construct is similar to the ?
operator in Rust, providing a concise and expressive way to propagate null
values.
// Returns the sum of optional values `n` and `m` or `null`, if either is `null`
func addOpt(n : ?Nat, m : ?Nat) : ?Nat {
do ? {
n! + m!
}
}
The following example defines a simple function that evaluates expressions built from natural numbers, division, and a zero test, encoded as a variant type:
type Exp = {#Lit : Nat; #Div : (Exp, Exp); #If : (Exp, Exp, Exp)};
func eval(e : Exp) : ? Nat {
do ? {
switch e {
case (#Lit n) { n };
case (#Div (e1, e2)) {
let v1 = eval e1 !; // If eval e1 returns null, exit with null
let v2 = eval e2 !; // If eval e2 returns null, exit with null
if (v2 == 0)
null ! // Explicitly exit with null for division by zero
else v1 / v2
};
case (#If (e1, e2, e3)) {
if (eval e1 ! == 0) // Unwrap and check if zero
eval e2 ! // Return result of e2 (or null if it's null)
else
eval e3 ! // Return result of e3 (or null if it's null)
};
};
};
};
let expr : Exp = #If(
#Div(#Lit 10, #Lit 2), // 10 / 2 = 5 (non-zero, so evaluate e3)
#Lit 0, // e2 (ignored because e1 ≠ 0)
#Div(#Lit 6, #Lit 3) // e3 → 6 / 3 = 2
);
eval(expr);
To guard against division by 0 without trapping, the eval
function returns an option result, using null
to indicate failure.
Each recursive call is checked for null
using !
, immediately exiting the outer do ?
block and then the function itself, when an intermediate result is null
.