Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Optional values

A value that may be missing has the type Optional<T>, written T? for short: either Value, holding a T, or Empty, holding nothing. It plays the role of Rust’s Option<T> and Swift’s Optional. Use it instead of a null reference or a sentinel such as -1 or "": the type tells the caller that the value can be absent, and reading it goes through a method that checks.

Optional is part of the runtime, so you can use it without an import. The Optional reference lists every member.

# A lookup that may find nothing returns an Optional instead of a sentinel.
struct Person {
    String name;
    int age;

    construct(this.name = "", this.age = 0) {}
}

find_adult(people: Array<Person>): Optional<Person> {
    for (var p in people) {
        if (p.age >= 18) { return Optional<Person>.Value(p); }
    }
    return Optional<Person>.Empty;
}

# `ulong?` is shorthand for `Optional<ulong>`.
index_of(text: String, wanted: String): ulong? {
    var i: ulong = 0;
    while (i < text.length) {
        if (text[i] == wanted) { return Optional<ulong>.Value(i); }
        i = i + 1;
    }
    return Optional<ulong>.Empty;
}

main(): int {
    var people = Array<Person>();
    people.append(Person("ada", 12));
    assert(find_adult(people).is_empty(), "no adult yet");

    people.append(Person("grace", 45));
    var adult = find_adult(people);
    assert(adult.is_value(), "grace is an adult");
    # An optional is a condition by itself: `if (adult)` tests is_some().
    if (!adult) { return 1; }
    assert(adult.value().name == "grace", "value() returns the Person");

    # Works for types that have no null, too.
    assert(index_of("pepper", "e").value() == 1, "first 'e' is at 1");
    assert(index_of("pepper", "z").is_empty(), "no 'z'");

    # value_or supplies a fallback instead of panicking.
    assert(index_of("pepper", "z").value_or(99) == 99, "fallback for Empty");

    # Optional is an enum with the cases Empty and Value(value).
    var nothing = Optional<int>.Empty;
    assert(nothing.is_empty(), "the Empty case is empty");
    var first_p = match (index_of("pepper", "p")) {
        Empty => -1,
        Value(at) => at as int,
    };
    assert(first_p == 0, "match on an optional");

    # An optional slot that is not given a value starts as Empty.
    var later: String?;
    assert(later.is_empty(), "unassigned optionals are Empty");
    return 0;
}

The T? shorthand

T? means Optional<T> wherever a type is written: x: int?, find(key: String): Node?, Array<Node?>, a field Token? leaf;. The ? applies to everything before it, so int[]? is an optional array, and Array<int?> is an array of optional values.

A C-style declaration inside a function body, such as Node? n = m;, is read as a declaration only when a name follows the ? and then =, ; or the end of the line; a ? b : c stays a conditional expression.

Returning early with ?

In a function that returns an optional, ? after an optional is its value, or, when it is Empty, returns Empty from the function at once, as ? does with an error for Result:

middle_name(person: Person): String? {
    var names = person.full_name?.split(" ");
    if (names.count < 3) {
        return Optional<String>.Empty;
    }
    return Optional<String>.Value(names[1]);
}

Using ? on an optional in a function that does not return an optional is an error ('?' requires the enclosing function to return an Optional).

Optional slots start as Empty

A variable or field of an optional type that is not given a value starts as Empty, not null:

struct Node {
    Token? leaf;   # Empty until assigned
}
var count: int?;   # Empty

Creating and checking optionals

MemberWhat it does
Optional<T>.Value(value)Creates a Value optional holding value.
Optional<T>.EmptyAn Empty optional.
is_value(), is_empty()Report which case the optional is.
!! (truth test)An optional is a condition by itself: if (found) is if (found.is_value()), and !found is found.is_empty().
value()Returns the value; panics with Optional.value on an Empty optional if the optional is Empty.
value_or(fallback)Returns the value, or fallback if the optional is Empty. Never panics.

Use value() when an Empty optional would be a bug in your program, since it stops the program. When a missing value is an expected outcome, test it first (if (found), is_value() or is_empty()), or supply a fallback with value_or. Testing the optional itself works because Optional declares the truth-test operator !! (see Truth tests).

Optional<T> works for every T, including types that have no null at all, such as int or ulong. For a struct type, a Value optional holds a reference to the object, like any other struct slot (see Memory and reference counting).

Optional<T> is an enum with the cases Empty and Value(value), so match and is work on it too:

var shown = match (name) {
    Empty => "nobody",
    Value(text) => text,
};
if (name is Value(text)) { print(text); }

An optional is a value enum: it lives inline in the variable, field or array element that holds it, so creating one allocates nothing, and copying one gives an independent optional. It is never null, and has no identity: compare two optionals with ==, which compares whether each holds a value and, if both do, their values; === on an optional is an error.

Optionals in the standard library

Standard library lookups that can come up empty return an optional rather than a sentinel value:

APIReturnsEmpty when
Array<T>.find(item)ulong?no element equals item
io.Path.parent()Path?the path is a root (/, C:\), a bare name with no root (a, ., ..), or empty
io.Path.file_name()String?the path is a root or empty, or its last component is . or ..
io.Path.file_stem()String?there is no file name
io.Path.extension()String?there is no file name, or the name has no dot after its first character (Makefile, .bashrc); foo. has the empty extension ""
io.Path.dot_index(name)ulong?name has no dot after its first character
http.Headers.get(name)String?no field has that name
HashMap<K, V>.try_get(key) (and Map, OrderedMap)V?key is absent
# Standard library lookups that may come up empty return an optional.
import io;

main(): int {
    # Array.find returns `ulong?`: the index, or none.
    var names = ["ada", "grace", "linus"];
    var at = names.find("grace");
    assert(at.value() == 1, "grace is at index 1");
    assert(names.find("ken").is_empty(), "ken is not in the array");

    # Path.parent, file_name, file_stem and extension return `Path?` or
    # `String?`: none when the path has no such part.
    var path = io.Path("docs").join("guide.md");
    assert(path.file_name().value() == "guide.md", "file name");
    assert(path.file_stem().value() == "guide", "stem");
    assert(path.extension().value() == "md", "extension");
    assert(path.parent().value() == io.Path("docs"), "parent");

    assert(io.Path("/").parent().is_empty(), "a root has no parent");
    assert(io.Path("notes").parent().is_empty(), "nor does a bare name");
    assert(io.Path("a/..").file_name().is_empty(), "'..' is not a file name");
    assert(io.Path("Makefile").extension().is_empty(), "no dot, no extension");

    # value_or supplies a default when none is an expected outcome.
    var ext = io.Path("Makefile").extension().value_or("txt");
    assert(ext == "txt", "fallback extension");
    return 0;
}

Breaking change. Array.find used to return a long that was -1 for “not found”, and the Path methods above returned an empty Path or String (or name.size for dot_index). Code written against the old API must test the optional instead: a.find(x) >= 0 becomes a.find(x).is_value(), p.parent().is_empty becomes p.parent().is_empty(), and a value is read with value() or value_or. A comparison such as a.find(x) == -1 still compiles but compares the optional object rather than an index, so search for these and rewrite them. String.find is unchanged and still returns -1.

Optional or Result

An Optional<T> says only that a value is missing. When the caller needs to know why, return a Result instead, whose Err carries an error.

Current limits

  • Some runtime APIs still use null references, default values or sentinels for “missing”: a HashMap’s get (and indexer) returns the default value for a missing key (use try_get for an optional), and String.find returns -1, for example.