Optionals
Declaration
let maybe: ?i64 = null;
let found: ?i64 = 42;?T means “either a value of type T, or null.”
Null checking
if (maybe != null) {
io.println("{maybe!}");
}Unwrap
let val: i64 = maybe!; // traps if nullThe ! operator unwraps the optional. If it’s null at runtime, the program traps.
An unwrapped optional is also a place you can write. It changes the value the
optional holds, and traps if there is none, so it is = without the ! that
gives a null optional a value:
var count: ?i64 = 1;
count! = 9; // count holds 9
count! += 1; // count holds 10
var name: ?string = "a";
name! = "b"; // the old string is dropped
count = null;
count! = 1; // traps: unwrap of null
count = 1; // fine: count holds 1 againThe optional has to be one you can change and has to live somewhere: writing
through a let is an error, and so is find(k)! = 1, which would store in a
temporary. Keep the result in a var first.
Default values with or
let val: i64 = maybe or { 0 };The or { } block provides a fallback value when the optional is null: its
last expression, written without a ;. Statements may come before it, and
the block may instead leave with return, break, continue or @trap.
With a ; after it (or { 0; }) the block has no value, and using it as one
is a compile error.
As function returns
fn find(items: []i64, target: i64) -> ?i64 {
for (i in 0..items.len) {
if (items[i] == target) { return i; }
}
return null;
}
let idx: ?i64 = find(data, 42);
if (idx != null) {
io.println("found at index {idx!}");
}A check does not unwrap: inside if (idx != null) the value is still a
?i64, so you write idx! there.
Comparing
An optional compares only with null. To compare its value, unwrap it
first:
if (idx != null && idx! == 3) { ... } // the value, once you know there is one
if (idx == 3) { ... } // compile error: an optional compares only with nullAn optional pointer (?*T) is the exception: it also compares with another
pointer.
Leaving early with or return
fn first_word_len(text: []const u8) -> ?i64 {
let end: i64 = bytes.index_of(text, " ") or return; // null: return null
return end;
}
fn log_user(id: i64) {
let u: *User = users.find(id) or return; // null: just return
io.println(u.name);
}A bare or return on a null returns null from a function that returns an
optional, and simply returns from a void function. Anywhere else there is
no value to return, so it is a compile error: write or return <value> or
handle it with or { ... }:
fn age_of(id: i64) -> i64 {
let u: *User = users.find(id) or return -1;
return u.age;
}Optional pointers
A ?*T is a pointer that may be null. It costs nothing over a *T (null is
its “none”), but you unwrap it before using what it points at:
let node: ?*Node = list.find(42);
let v: i64 = node.value; // error: field access '.value' of '?*Node', which may be null:
// unwrap with `node!` or check `node != null` first
if (node != null) {
io.println("{node!.value}"); // `!`: the check above says it cannot trap
}
let n: *Node = list.find(42) or return; // or bind it once, unwrapped
io.println("{n.value}");Field access, method calls, indexing, *p, and passing a ?*T where a
*T is expected all need the ! (or an or). Comparing with null does
not.
Optional fields
A struct field, a tagged-union payload field or an array element can be optional. It holds its value in place; declare = null as its default and a literal can leave it out:
struct User { name: string; nick: ?string = null; age: ?i64 = null; }
var u = User { name: "ada" }; // nick and age are null (their defaults)
u.nick = "countess";
if (u.nick != null) { io.println(u.nick!); }
let shown: string = u.nick or { u.name };
let saved = u.nick; // a copy: changing u.nick later leaves it alone
u.nick = null; // the old string is freedCopying a User copies its nick; dropping one frees it.
Difference from error returns
?T— value might be absent (no error info, just “nothing”)T or E— operation can fail with a specific error value
Use ?T for lookups and optional fields. Use T or E for operations that can fail with diagnostic information.
Last modified: