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— notT?. - 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) ?? 0 | match 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");
}
| Fallback | On absence | Requires |
|---|---|---|
?? value | the expression's result is value | value converts to the payload type |
?? return e | the function returns e | e fits the function's return type |
?? break | the innermost (or labelled) loop ends | an enclosing loop |
?? continue | the next iteration starts | an enclosing loop |
?? fail e | the function fails with e | a 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, postfix | c ? 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 ?? 0iserror: 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??iscatch { else => fallback }; see Handling failures. - There is no
??=.cached ??= 5;does not parse; writecached = cached ?? 5;. ??is compiler-owned control flow. It cannot be declared in anextendblock 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
- Optionals — the type
T?and its patterns - Optional propagation —
?, when absence should reach the caller - Handling failures —
catch, the fallback for a fallible - Learn: Coalesce, Coalesce exit, Absence to error