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:
if o { … }, for theifbranch, andif !o { … } else { … }for theelsebranch;while o { … }, for the loop body;guard o else { … }, for the rest of the block;assert(o), for the rest of the block;- a comparison with
null, as inif o != null.
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
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.
- If the answer is something that already exists, such as an element of an array, a node in a tree or a value in a table, return an optional. It is a reference to the thing, so nothing is copied and the caller can modify it.
- If the answer is a new value, such as a parsed number or a computed
result, return the value and a
bool, likeparse_intdoes. An optional cannot point at a value that was only just computed, because the value would die with the function.
#Exercises11.5
-
Write
fn find_name(flock: Goose[:], name: u8[:]) -> Goose?and use it to rename one goose in an array, if it is there. -
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. -
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)