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.