Chapter 11

Optionals

References are never null. That is a good property, but sometimes "there is nothing here" is exactly what you need to say: the search found nothing, the list has ended, the field has not been set. For those cases Goose has the optional, T?, which is a reference that may be null. This chapter is short, because optionals are simple, but they appear in almost every program.

#A reference that may be null11.1

T? is an optional T: either a reference to a T, or null. It is represented as a plain address, with null as address 0, so it takes no more space than a reference. A function that searches for something returns one:

struct Goose { name: u8[..12], weight: f32 }

fn find_heavier(flock: Goose[:], than: f32) -> Goose? {
    for &g in flock {
        if g.weight > than { return g; }
    }
    return null;
}

fn main() {
    var flock: Goose[>..] = [Goose { "Ada", 4.5 }, Goose { "Bea", 5.25 }];
    let big = find_heavier(flock, 5.0);
    if big { print("found ", big.name); } else { print("nobody"); }
    let huge = find_heavier(flock, 9.0);
    if huge { print("found ", huge.name); } else { print("nobody"); }
}
found Bea
nobody

Because an optional is a reference, the result is the element of flock, not a copy of it. You can write through it once you know it is not null.

T? composes with any type. i64? is an optional reference to an i64, and an optional struct field that has no default is null when a literal leaves it out.

#Narrowing11.2

An optional is not transparent. You cannot use it as its target until you have checked that it is not null:

fn first_over(xs: i64[:], min: i64) -> i64? {
    for &x in xs { if x > min { return x; } }
    return null;
}

fn main() {
    let xs: i64[3] = [3, 8, 5];
    let big = first_over(xs, 4);
    print(big + 1);
}
error: operator + cannot be applied to i64? and i64

Checking it narrows it. Inside the region where the compiler knows it is not null, the optional behaves as a plain reference: transparent and writable. These all narrow:

fn first_over(xs: i64[:], min: i64) -> i64? {
    for &x in xs { if x > min { return x; } }
    return null;
}

fn main() {
    var xs: i64[3] = [3, 8, 5];
    let big = first_over(xs, 4);
    if big { big = 0; }              // writes xs[1]
    print(xs);
    let small = first_over(xs, 2);
    assert(small);                   // stops the program if it is null
    print(small + 1);
}
[3, 0, 5]
4

guard reads especially well with optionals, because it lets the main path continue at the same indentation:

struct Goose { name: u8[..12], weight: f32 }

fn weight_or_zero(g: Goose?) -> f32 {
    guard g else { return 0.0; }
    g.weight                         // g is a Goose& from here on
}

fn main() {
    var ada = Goose { "Ada", 4.5 };
    print(weight_or_zero(ada), " ", weight_or_zero(null));
}
4.5 0.0

Note how ada, a plain variable, is passed where a Goose? is expected. A variable binds to an optional parameter as it would to a reference parameter, with no &.

#Narrowing ends when you rebind11.2.1

Narrowing is a property of a region of code, so it ends when the optional might have changed. Rebinding the optional with .= ends it, even to something that is obviously not null, and setting it to null certainly does:

fn main() {
    var n = 5;
    var o: i64? = n;
    if o {
        o .= null;
        print(o + 1);
    }
}
error: operator + cannot be applied to i64? and i64

The exception is a while loop whose condition is the test. The condition runs again before every iteration, so the body can move the optional along and still find it narrowed on the next round. That is how you walk a chain of links:

struct Link { val: i64, next: Link? }

fn main() {
    var c = Link { val: 3, next: null };
    var b = Link { val: 2, next: c };
    var a = Link { val: 1, next: b };
    var cur: Link? = a;
    var sum = 0;
    while cur {
        sum += cur.val;
        cur .= cur.next;
    }
    print(sum);
}
6
1 a: Link 2 b: Link 3 c: Link null cur a Link? is a reference that may be null: no extra space, null is address 0
Each next field is an optional link. The cursor starts at a and moves along until it reaches null.

cur .= cur.next rebinds the cursor to the next link, which may be null. The while re-tests it before the next iteration.

#Optionals as fields11.3

An optional field with no declared default is null when a literal leaves it out, and prints as null:

struct Nest { eggs: i64, mother: Nest? }

fn main() {
    var n = Nest { eggs: 3, .. };
    print(n);
    var m = Nest { eggs: 0, .. };
    n.mother .= m;
    let mom = n.mother;
    if mom { print("has a mother with ", mom.eggs, " eggs"); }
}
Nest { 3, null }
has a mother with 0 eggs

Notice the extra step: the field is copied into a local, mom, and the local is narrowed. The compiler narrows variables, not arbitrary expressions, because a field could be changed by something else between the check and the use. Copying an optional is cheap, since it is just an address.

The rule from Chapter 8 still applies: a reference stored in a field must not outlive what it points at. Here n and m live in the same scope, so storing m in n.mother is fine. Chapter 13 shows the usual way to build linked structures, where all the nodes live in one array and the links point within it.

#Optionals or a trailing bool?11.4

Goose has two common ways to say "maybe": an optional, and a trailing bool result. The choice follows from what you are returning.

#Exercises11.5

  1. Write fn find_name(flock: Goose[:], name: u8[:]) -> Goose? and use it to rename one goose in an array, if it is there.

  2. Write a function that takes an i64[:] and returns an optional reference to its last negative element. Test it on an array with negative numbers and on one without.

  3. Why does this not compile, and how would you fix it?

```goose error fn make() -> i64? { var x = 5; return x; }

fn main() { print(make() != null); } ```

error error: returning a reference rooted in x, which dies with this function (§9.2)