Optionals
Optionals
A type followed by ? may hold a value or nothing. This is the language's
answer to null, and the analyzer checks it.
let content: string? = fs::read_file(path)Write Type?. There is no Option<Type> form, and ? composes with any type,
including generics and other optionals: Array<string>? is an optional array,
and string?? is legal though rarely useful.
Reading an optional
Test with is_value(), then read with unwrap():
let content: string? = fs::read_file(path)
if !content.is_value() {
return None
}
let text = content.unwrap()unwrap() on a non-value terminates the program. It prints a failure
backtrace and exits; it does not return a default and it cannot be caught. So the
guard is not optional in practice. The usual shape is an early return on the
empty case, which leaves the rest of the function working with a plain value.
An optional has three states, not two: it holds a value, it is None, or it is
an error carrying a message. is_value, is_none, and is_error tell them
apart.
None
None is the empty value. It is what an optional holds when it has nothing, and
what a function returns to signal absence:
fn first_segment(path: string) => string? {
let cut = path.index_of("/")
if cut < 0 {
return None
}
return path.substring(0, cut)
}Returning a plain value where an optional is expected is fine: the value is
accepted directly, as the return path.substring(...) above shows. The reverse
is not. An optional cannot be used where a plain value is required until it has
been unwrapped.
Optional fields
A field that may be absent is declared the same way and initialized to None:
class AgentHolder {
child: process::Process?
constructor() {
this.child = None
}
}Unwrapping with ?
A postfix ? unwraps an optional. If the value is present it becomes the
result; if it is None or an error, the ? stops the current function and
sends that outcome up to the caller:
fn read_config(path: string) => string? {
let text = fs::read_file(path)?
return text.trim()
}The call reads as though read_file returned a plain string, because the
absent case has already left the function. This is the compact form of the
guard-and-early-return shape, and it composes: several ? in a row each bail out
on their own.
Providing a fallback with else
An else block placed directly after the ? handles the empty case instead of
propagating it:
let text = fs::read_file(path)? else {
"default contents"
}The block runs when the value is None or an error, and what it produces becomes
the result. The else has to follow the ? immediately, since it belongs to the
unwrap rather than to any surrounding statement.
Use ? on its own when the caller should deal with the absence, and ? else
when this function can supply something sensible and carry on.
Error
Error builds a failure carrying a message:
return Error("failed to parse the manifest")It takes exactly one string argument. An error travels the same path an empty
optional does, so a ? propagates it to the caller and a ? else catches it,
which is what lets one return type describe both "nothing" and "something went
wrong".
Optionals from the standard library
Many standard library calls return an optional rather than a sentinel, which is what makes the absent case impossible to ignore:
let found: json::JsonValue? = object.get("name")
let line: string? = child.read_line()
let text: string? = fs::read_file(path)Conversions on the root object type follow the same pattern. to_bool,
to_char, and the other renderings return an optional, because not every value
has a meaningful rendering, and the default is None.