Optionals

Declaration

tape
let maybe: ?i64 = null;
let found: ?i64 = 42;

?T means “either a value of type T, or null.”

Null checking

tape
if (maybe != null) {
    io.println("{maybe!}");
}

Unwrap

tape
let val: i64 = maybe!;  // traps if null

The ! 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:

tape
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 again

The 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

tape
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

tape
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:

tape
if (idx != null && idx! == 3) { ... }   // the value, once you know there is one
if (idx == 3) { ... }                   // compile error: an optional compares only with null

An optional pointer (?*T) is the exception: it also compares with another pointer.

Leaving early with or return

tape
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 { ... }:

tape
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:

tape
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:

tape
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 freed

Copying 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: