Sum Types
A sum type A | B holds one value whose type is one of a set of distinct types, its members. A value of int32 | bool is either an int32 or a bool, and it remembers which. Like optionals and fallibles, sums are compiler-owned: they need no declaration, and the members can be any types — primitives, structs, variants, slices, other native forms.
sum-type = type "|" type { "|" type }
struct Circle {
radius: float64;
}
struct Square {
side: float64;
}
struct Rectangle {
width: float64;
height: float64;
}
type Box = Square | Rectangle;
type Shape = Circle | Box;
func Area(shape: Shape) -> float64 {
return match shape {
c: Circle => 3.14159 * c.radius * c.radius,
s: Square => s.side * s.side,
r: Rectangle => r.width * r.height
};
}
A sum is taken apart only by a pattern, and tested with is.
Sums, variants and unions
Rux has three ways to say "one of several":
| Form | Alternatives are told apart by | Tag |
|---|---|---|
a sum A | B | their types | yes, hidden |
a variant | case names, declared once | yes, hidden |
a union | nothing — fields overlap | none |
Use a sum when the alternatives are already types and their types say everything — ParseError | IoError, Options | Defaults. Use a variant when two alternatives could have the same payload type, or when the set needs a name of its own that callers depend on.
The type is a set
A sum is a set of resolved types, not a list. The compiler normalizes every sum it sees:
| Rule | So |
|---|---|
| aliases are resolved | bool | int32 is bool8 | int32 |
| order does not matter | int32 | bool and bool | int32 are one type |
| duplicates are removed | bool | int32 | bool is bool8 | int32 |
| nested sums are flattened | int32 | (bool | int32) is bool8 | int32 |
| a single remaining member is that type | int32 | int32 is int32 |
flattening stops at ? and ! | int32 | ((bool | int32)?) has two members |
Members are kept in a canonical order derived from their fully qualified names, which is why diagnostics print bool8 | int32 whichever order the source used.
| binds looser than the postfix suffixes and tighter than !: int32 | bool? is rejected until it is grouped as int32 | (bool?) or (int32 | bool)?, and Options | Defaults ! IoError is (Options | Defaults) ! IoError.
Making a sum value
A value enters a sum in one of two ways, where a sum is expected — an annotated let, an assignment, an argument, a return value:
- Injection. A value whose type is exactly one of the members becomes that member.
- Widening. A value of a smaller sum whose members are all members of the target keeps its active member.
func Tile(side: float64) -> Box {
return Square { side: side }; // injection: Square is a member of Box
}
let shape: Shape = Tile(3.0); // widening: Square | Rectangle into Circle | Rectangle | Square
No other conversion is searched. A member is never converted to make it fit, so let s: int32 | bool = 2.5; is error: cannot assign 'float64' to 'bool8 | int32', and a wider sum never narrows:
error: cannot assign 'A | B | C' to 'A | B'
Knowing the value is an A right now is not enough; take it out with a subset pattern instead.
An unsuffixed integer or float literal targets the one member of its kind, whatever its value. With two integer members it is ambiguous:
error: integer literal is ambiguous for 'int32 | int64', which has several integer members; add a suffix or a cast
Write 5i64 or 5 as int32. Every other literal is injected only when its own type is a member.
Using a sum
A sum has none of its members' operations — not even those they all share. Arithmetic, field access, method calls and Display all need the member first:
error: operator '+' cannot combine left operand 'bool8 | int32' with right operand 'int'
error: argument 2 to 'PrintLine' has type 'bool8 | int32', but variadic parameter 'args' requires 'Display'
What a sum does have:
| Operation | Meaning |
|---|---|
a match | selects the member and binds it |
is | tests which member is active, without taking it out |
==, != | equal when the same member is active and the payloads are equal |
| assignment, passing, returning | moves or copies the whole value, as for any type |
== needs two operands of the same sum type. An unsuffixed literal adopts the sum's type, so port == 8080 works for port: int32 | bool, but a value with a type of its own is never injected for a comparison:
error: operator '==' cannot compare 'bool8 | int32' with 'bool8'
note: both operands of a native comparison have the same type; a comparison never injects, widens, or wraps an operand
Give the value the sum type first — let off: int32 | bool = false; — and compare two sums. Two values holding different members are unequal even when the values look alike: 7i8 and 7i64 in an int8 | int64 | bool differ.
Like the other native forms, a sum cannot be extended and implements no interface, even when every member implements it. A type alias of a sum names exactly its members and hides none of them.
Generic sums
A sum may be written over type parameters, T | U, and is normalized again for every instantiation. When the arguments coincide, the sum collapses:
func Choose<T, U>(left: bool, a: T, b: U) -> T | U {
if left {
return a;
}
return b;
}
let same: int32 = Choose<int32, int32>(false, 1, 2); // T | U is int32
let mixed = Choose<int32, bool>(false, 1, true); // bool8 | int32
A match over T | U stays valid at every instantiation by ending in else; see Sum patterns.
Layout
A sum is stored as an 8-byte tag naming the active member, followed by that member's payload: sizeof(int32 | bool) is 16, and sizeof(Circle | Rectangle) is 24. The tag values are not a stable ABI. See Layout.
See also
- Sum patterns — taking a member out
- Type tests —
is - Variants and Unions — the named and the untagged alternatives
- Errors — error sums
T ! (A | B) - Learn: Sum type, Sum widening, Generic sum