Anonymous sums
A | B holds one owned value of either whole type. It works in ordinary
bindings, fields, arguments and returns, including Result[T, A | B] errors.
The compiler stores a tag and the largest active payload inline; wrapping and
widening allocate nothing. Payload construction retains its own costs.
Aliases, order, duplicates and nested sums normalize: A | B, B | A and
(A | B) | A are identical; A | A is just A. Distinct nominal classes and
enums remain distinct, and enum variants never become separate members.
There is no stable tag numbering or foreign layout contract.
Structural formatting (print, str.repr, and std.fmt.represent) shows the
active member directly, without an anonymous wrapper. For example, a
Result[(), IoError | ThreadError] error can be represented as
Result[(), IoError | ThreadError].Err(ThreadError.System(11)). Type names use
the normalized A | B spelling. The structural fallback for an error returned
by main follows the same rule; named enums keep their usual qualified variants.
A closure signature such as OnceClosure[[], i64] names no concrete
environment, so it is a member only in a function’s declared return type,
directly or inside a member such as SpawnError[OnceClosure[[], i64]]. The
body’s returned or propagated closure resolves it; see
closure return inference.
| binds less tightly than references and type applications. Use &(A | B)
to borrow a sum. &A | B attempts to store a reference as a member and is
rejected, as are other members forbidden by owned storage rules. Callable
brackets delimit their types: Callable[[A | B], C | D]. Parentheses without
a comma group a type; (A,) is a singleton tuple and () is unit.
An expected type permits lossless injection of a member or widening to a
superset. This applies to annotated bindings, assignments, arguments,
constructors and returns. Result[T, E] can widen to Result[T, F] when
F contains every member of E; its success type must remain identical.
Other generic containers and references do not gain covariance. Conversions
evaluate their source once, move owned payloads and invoke no user conversion.
Public return types and binding types remain declared, never silently widened.
A constructor, tuple or collection literal takes its context from the one
member it can build, so [1.0, 2]? passed as list[f64] | Unset checks its
elements as f64, and []? or {}? needs no further annotation. Several
viable members, such as list[f64] | list[i64], are ambiguous and rejected;
bind the value with a member type first.
Match a whole member with case name: MemberType:. An ordinary wildcard
matches any remaining value. A member annotation names a single normalized
type; pattern | means alternatives, not a type union. Typed member patterns
are supported at the case position, not inside other patterns.
An enum variant pattern also selects its member directly, including inside
Err or another payload. The compiler tests the member before testing its
variant, so equal variant names in different enums remain distinct:
def parse(text: &str) -> Result[i64, AllocError | ParseError]:
Ok(i64.parse(text)?)
def main() -> Result[(), IoError]:
match parse("x"):
case Ok(value):
print(value)?
case Err(ParseError.Invalid):
print("not a number")?
case Err(ParseError.OutOfRange):
print("outside i64 range")?
case Err(_):
print("out of memory")?
Ok(())
not a number
Every member and each enum member’s variants must be covered, whether by variant patterns, a whole-member pattern, or a wildcard. Duplicate and unreachable cases are rejected; covering one variant does not cover its siblings. Payload literals still need a fallback for the other payload values.
The matched value supplies generic arguments, as in ordinary enum patterns.
If a qualifier names several members, such as Choice[i64] | Choice[str],
Choice.Value(value) is ambiguous even when only one payload type would fit
its subpattern. Use a whole-member pattern with an explicit type and match
that value’s variants inside the arm. Case guards remain unsupported.
An owned match moves noncopyable payloads. Matching &value or &mut value
borrows the active member with the same permission and lifetime. Only the
active payload is initialized or destroyed. Copyability and thread transfer
require every alternative to qualify. If generic substitution collapses
T | A to A, case value: A: binds that ordinary value; two equivalent
member arms are still duplicates. Generic inference rejects ambiguous member
substitutions; supply explicit type arguments in that case.
For reporting, a sum satisfies
std.fmt.Formattable when every normalized member
satisfies that protocol. Formatting dispatch borrows the active member without
allocation or consuming the sum. A missing or mismatched method on any member
rejects the whole bound, regardless of which member is active.
This version does not infer residual sums after recovery, introduce empty
sums, forward arbitrary member methods or protocols, or erase errors. See
propagation for ? and builtin
Failure, the last-resort error that erases its cause.
The anonymous-sum proposal records the design rationale and migration boundary. The composition overview distinguishes sums from deliberate error mapping and typed stream APIs.
Collection keys
A sum satisfies KeyType when every member is a supported key type.
Use set[i64 | str] or dict[i64 | str, V] directly. Hashing and equality
include the active member type and its structural value: 1, 1i8, True
and "1" stay distinct in a sum that admits them. Normalized aliases and
member order do not change identity. Keys with an unsupported member are
rejected even if that member is never inserted.
Insertion and lookup use the ordinary expected-type injection rules. Observed member values can be injected for lookup without moving them; their source remains borrowed through the operation. This does not make reference types covariant. Sums containing move-only classes keep their ownership rules, and stored keys are only exposed through shared borrows. Anonymous sums use the hash-table representation, including sums with finite members.