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
| Member | What it does |
|---|---|
Optional<T>.Value(value) | Creates a Value optional holding value. |
Optional<T>.Empty | An 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:
| API | Returns | Empty 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’sget(and indexer) returns the default value for a missing key (usetry_getfor an optional), andString.findreturns-1, for example.