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":

FormAlternatives are told apart byTag
a sum A | Btheir typesyes, hidden
a variantcase names, declared onceyes, hidden
a unionnothing — fields overlapnone

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:

RuleSo
aliases are resolvedbool | int32 is bool8 | int32
order does not matterint32 | bool and bool | int32 are one type
duplicates are removedbool | int32 | bool is bool8 | int32
nested sums are flattenedint32 | (bool | int32) is bool8 | int32
a single remaining member is that typeint32 | 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:

OperationMeaning
a matchselects the member and binds it
istests which member is active, without taking it out
==, !=equal when the same member is active and the payloads are equal
assignment, passing, returningmoves 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