Coalescing

The coalescing operator ?? opens one level of an optional. When the optional is present, the result is its payload; when it is absent, the result is the fallback.

coalesce-expr = expr "??" fallback
fallback      = expr | "return" [ expr ] | "break" [ label ] | "continue" [ label ] | "fail" expr

Semantics

func Lookup(key: int32) -> int32? {
    return key % 2 == 0 ? .Some(key * 10) : none;
}

func Slow() -> int32 {
    PrintLine("  (computing the fallback)");
    return -1;
}
PrintLine("{}", Lookup(4) ?? Slow()); // 40
PrintLine("{}", Lookup(3) ?? Slow()); // (computing the fallback), then -1
  • The left operand is evaluated exactly once, and must be a native optional T?.
  • The result has the payload type T — not T?.
  • The fallback is checked at compile time but evaluated only when the optional is absent. A slow or visible fallback, such as Slow() above, costs nothing when a value is present.
  • The fallback must convert to T, or leave (below).

a ?? b is shorthand for a match:

With ??Means
Lookup(k) ?? 0match Lookup(k) { v? => v, none => 0 }

One level at a time

?? opens only the outer level. On an int32??, the result is an int32?, and a present absence is a present value, not absence:

let nested: int32?? = .Some(none);
let once = nested ?? .Some(1i32); // none: the outer level was present

To open both levels, coalesce twice with explicit grouping: (nested ?? none) ?? 0, or two lets.

Chaining

?? is right-associative, so a chain tries each optional from the left, and the first present one wins:

PrintLine("{}", Lookup(3) ?? Lookup(5) ?? Lookup(6) ?? 0); // 60

This is Lookup(3) ?? (Lookup(5) ?? (Lookup(6) ?? 0)): every operand but the last is an optional, and each later one runs only if everything before it was absent.

A fallback that leaves

Instead of a value, the fallback may leave: return, break, continue, fail error, a call to Panic, or a call to any #NoReturn() function. A leaving fallback is the whole right operand, and it never decides the result type — the result is still the payload:

// Skip an absent value and try the next key.
func FirstEven(keys: int32[..]) -> int32 {
    for key in keys {
        let value = Lookup(key) ?? continue;
        return value;
    }
    return 0;
}

// Leave the function early.
func OrZero(key: int32) -> int32 {
    let value = Lookup(key) ?? return 0;
    return value + 1;
}

// Turn absence into a failure.
func Required(key: int32) -> int32 ! NotFound {
    return Lookup(key) ?? fail NotFound { key: key };
}

// Absence here is a bug.
func Trusted(key: int32) -> int32 {
    return Lookup(key) ?? Panic("the key is always even");
}
FallbackOn absenceRequires
?? valuethe expression's result is valuevalue converts to the payload type
?? return ethe function returns ee fits the function's return type
?? breakthe innermost (or labelled) loop endsan enclosing loop
?? continuethe next iteration startsan enclosing loop
?? fail ethe function fails with ea fallible function
?? Panic(msg)the program stops—

Each leaving form follows its own rules, so ?? continue outside a loop is error: 'continue' can only be used inside 'while', 'for', or 'loop', and ?? fail in a function that cannot fail is error: 'fail' needs an enclosing fallible function, but this function returns 'int'. ?? return none needs a function that returns an optional. ?? fail is the standard way to turn absence into an error; see Error propagation.

Precedence

?? sits between the conditional operator and || in the precedence table:

Tighter than ??Looser than ??
||, &&, comparison, arithmetic, as, is, unary, postfixc ? a : b, assignment, ranges

So every comparison and arithmetic operator next to a ?? belongs to the fallback:

let mild = (Lookup(3) ?? 0) == 0;

Without the parentheses, Lookup(3) ?? 0 == 0 means Lookup(3) ?? (0 == 0), a bool fallback for an int32 payload:

error: coalescing fallback has type 'bool8', but the optional payload is 'int32'

Likewise flag ?? false || true is flag ?? (false || true).

Restrictions

  • The left operand must be a native optional. A plain value is never absent: count ?? 0 is error: operator '??' requires an optional left operand, but found 'int32'. A raw pointer is rejected the same way, even though it may be null; make it an optional pointer (*T)? first.
  • A fallible is rejected, because coalescing would silently throw its error away:
    error: operator '??' cannot take 'int32 ! E'
      note: coalescing tests one optional level and would silently discard an error
      help: recover the error with 'catch', or propagate it with '?'
    

    The fallible counterpart of ?? is catch { else => fallback }; see Handling failures.
  • There is no ??=. cached ??= 5; does not parse; write cached = cached ?? 5;.
  • ?? is compiler-owned control flow. It cannot be declared in an extend block or overloaded.

Ownership

?? consumes the optional it is given. A named optional of a copyable type is copied and stays usable. A named optional of a move-only type must be moved explicitly, and so must a move-only fallback:

let h = (<-maybe) ?? <-fallback;

Without the <- the compiler stops with error: move-only value 'h' requires an explicit '<-' in coalescing operand. The fallback is moved only when it is used, so after this line fallback is possibly moved, and is destroyed at the end of its scope if it was not. A borrowed optional — a &T? — is never an operand; match it instead. See Copy and move.

See also