Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Standard sums and propagation

Option[T].unwrap_or(default: T) -> T selects a present payload or the default. Option[T].ok_or(error: E) -> Result[T, E] converts Some(value) to Ok(value) and Nothing to Err(error). Both consume owned receivers, evaluate receiver then argument exactly once, and drop the unused argument. They add no allocation and support affine payloads and arguments; their construction expressions retain their own allocation contracts. Unit payloads and unit errors are supported. Bare absence needs enough type context to determine T. Conversion is explicit: use option.ok_or(error)? in a Result-returning function; ? alone does not invent an error for absence.

Defaults and recovery

Result[T, E].unwrap_or(default: T) -> T returns the Ok payload or the supplied default, discarding the error. Like the Option form, it evaluates the receiver and default eagerly, in that order. In option.unwrap_or(compute()?), compute runs and can propagate an error even when the option is Some. Choose an eager default only when that is intended.

The lazy alternatives consume an owned callback and invoke it at most once:

MethodCallbackWhen calledResult
Option[T].unwrap_or_else(f)() -> TNothingT
Result[T, E].unwrap_or_else(f)E -> TErr(error), passing the errorT
Option[T].ok_or_else(f)() -> ENothingResult[T, E]

Named functions, reusable closures, and owned one-shot closures use OnceCallable. Constructing a callback and evaluating its captures still happens eagerly; only its body is conditional. Unused callbacks and their captures are dropped. These operations add no allocation, deep copying, or implicit error propagation. The existing restrictions on passing borrowed environments by value apply. Use match for reference-carrying payloads and callbacks that cannot be passed as owners.

Both sums also support map_or(default, mapper) and map_or_else(fallback, mapper). The mapper consumes T and returns U. map_or evaluates its default U eagerly. map_or_else invokes only the selected callback: Option’s fallback takes no arguments, Result’s fallback consumes E, and both return the same U as the mapper. Receiver and callback expressions evaluate from left to right, once. No automatic flattening occurs.

For a fallible default, choose U = Result[T, E], wrap a present value with Ok, and propagate after selection:

def default_number() -> Result[i64, ParseError]:
    i64.parse("23")

def choose(value: Option[i64]) -> Result[i64, ParseError]:
    Ok(value.map_or_else(default_number, Ok)?)

def main() -> Result[(), ParseError | IoError]:
    print(choose(Some(7))?, choose(Nothing)?)?
    Ok(())
7 23

These fallback semantics follow Rust’s Option and Result operations, with Plenty’s explicit allocation and callback contracts.

Deliberate assertions

Option[T].expect(message: str) -> T and Result[T, E].expect(message: str) -> T return the success payload or trap with the supplied message and the call site’s source location. The message is evaluated eagerly, including on success. Reporting adds no allocation; constructing a dynamic message retains its ordinary allocation contract. The trap reports the message, without formatting the error payload, and uses Plenty’s existing trap behavior rather than Rust panic unwinding. Prefer ? and explicit recovery for expected failures.

Inspection and propagation

Result[T, E].is_ok() -> bool and is_err() report which variant a result holds; Option[T].is_some() -> bool and is_none() do the same for an option. They observe the receiver, so a named value remains usable afterwards and a temporary is dropped at the end of the full expression. They take no arguments, ignore the payload, and allocate nothing. Asking a Result for is_some or an Option for is_ok is a compile-time error that names the available pair. Classes may define methods with the same names.

Option[T] has Some(value) and Nothing; Result[T, E] has Ok(value) and Err(error). These prelude variants are ordinary typed values. Some can infer its type from its payload; Ok, Err, and Nothing need expected context for their missing type arguments. Qualified constructors and aliases are supported. Payload storage follows enum rules.

Constructing, passing, returning, matching, and propagating standard sum wrappers allocates nothing. Payload operations retain their own costs and ownership rules. Wrapping a generator or closure does not make it copyable. References cannot be sum payloads. Nested sums preserve all scalar values; there is no source-visible null or numeric sentinel reserved for absence.

Postfix value? evaluates once. Some(value) or Ok(value) produces its payload. Nothing returns Nothing from the enclosing function; Err(error) returns Err(error). The function must return the same sum family; Result errors must be identical or widen losslessly into a declared anonymous sum, except for explicit Failure erasure below. Success types may differ. There are no implicit Result/Option conversions. A unit success produces unit.

For explicit error conversion, result.map_error(mapper) consumes Result[T, E] and returns Result[T, G], invoking the mapper only for Err. It forwards Ok without copying and adds no allocation. A payload variant is a mapper, as in print(line).map_error(AppError.Io)?. The std.error.map_error(result, mapper) function has the same behavior. Neither changes the propagation rule.

def first(values: &list[i64]) -> Option[i64]:
    Some(values.get(0)?)

def checked_first(values: &list[i64]) -> Result[Option[i64], OverflowError[i64]]:
    match first(values):
        case Some(value):
            Ok(Some(value + 1))
        case Nothing:
            Ok(Nothing)

def main() -> Result[(), AllocError | IoError | OverflowError[i64]]:
    values = [3]?
    print(checked_first(&values)?)?
    Ok(())
Option[i64].Some(4)

Here first is a focused demonstration of Option propagation; an ordinary first-element lookup can simply return values.get(0). Absence and arithmetic failure are distinct: checked_first returns Ok(Nothing) for an empty list and propagates overflow as Err, never as absence. ? does not implicitly convert Option to Result.

Propagation may appear in arguments, conditions, loops, and comprehensions. It composes with other postfix operations: values?[0] indexes the unwrapped value. Failure releases already evaluated pending operands and initialized locals, including hidden iterators/builders and entered resource scopes, in ordinary cleanup order. Later operands and statements do not run. Error payload ownership transfers to the caller. Generator bodies reject ? because their declared return family is Generator[T]; explicitly match instead.

Prefer a concrete error or a declared sum when combining fallible operations, so callers can inspect or report the cause. Builtin Failure is the last resort, for when no precise error can be written. It is a copyable allocation-free builtin enum containing only Failure.Unspecified. In a function returning Result[T, Failure], ? accepts any Result error type. Failure consumes and drops the original error payload, then returns Err(Failure.Unspecified) after normal cleanup. Success keeps its original payload. Erasure keeps no details, allocates no wrapper, does not catch traps, and does not convert Nothing. User cleanup retains its own effects and allocations.

Erasure applies only at ?. Assignments, arguments, direct returns, and explicit Err(payload) still require the declared error type or lossless anonymous-sum membership/widening. Use Err(Failure.Unspecified) to construct that error and Ok(()) for unit success. An allocating expression propagated this way still has its own AllocError type before propagation.

An expression statement cannot implicitly discard a Result, including in non-final branches, loops, context blocks, or generator bodies. Handle it, return it, or use explicit drop(result). Unused bindings, Option, and aggregates containing Results are not rejected by this rule. Builtin print also rejects a direct or borrowed Result; wrapper inspection uses str.repr(result) with its formatting Result handled separately.