Sum types · Lesson 10.1

Sum type

Source
Store a value that is one of several types — int32 | bool — and see that a sum is a set of types, where order and duplicates do not matter.

A sum type holds one value whose type is one of a short list. int32 | bool reads "an int32 or a bool": a variable of that type can hold 8080 now and true later, and it always knows which of the two it is holding.

You have met the idea twice already. A variant holds one of several cases, and an error sum let a function fail in two unrelated ways. A sum type is the general form: any types at all, joined with |. This lesson builds sums, stores them and compares them. Getting a member back out takes a pattern, and that is the next lesson.

A value of one of several types

A setting in a configuration file might be a number, a switch or a word. A type alias gives that sum a short name — it is still exactly int32 | bool | char8[..], just easier to write:

type Setting = int32 | bool | char8[..];

A value of any member type goes straight into the sum, with nothing to wrap it in:

let port: Setting = 8080;
let verbose: Setting = true;
let name: Setting = "server";

The value picks the member. 8080 is an integer, and Setting has exactly one integer member, so port holds an int32. true can only be the bool, and "server" only the text.

flowchart LR
    v["A value meets<br/>a sum type"] --> q{"Is its type<br/>a member?"}
    q -- "yes" --> ok["That member<br/>becomes active"]
    q -- "an unsuffixed literal" --> lit{"How many members<br/>of its kind?"}
    lit -- "exactly one" --> ok
    lit -- "several" --> amb["error: integer literal<br/>is ambiguous"]
    q -- "no" --> err["error: cannot assign<br/>'float64' to …"]

Switching members

A var sum can change its member, not only its value. Each assignment replaces both the value and the record of which member is active:

var limit: int32 | bool = 100;
limit = false;
limit = 250;

limit starts as the number 100, becomes the switch false, and ends as the number 250. A plain int32 variable could never hold false, and a bool could never hold 250; the sum can hold either.

A set of types

A variant names its cases. A sum names only types, so its list is a set: the order the types are written in does not matter, and a type written twice counts once. These are three spellings of one type:

let reordered: bool | int32 = limit;
let repeated: int32 | bool | int32 = reordered;

No conversion happens on either line, because there is nothing to convert — limit, reordered and repeated all have the same type. The compiler even writes every sum in its own sorted order in error messages, so int32 | bool appears there as bool8 | int32 (bool8 is bool's full name).

The same rule makes a sum of one type just that type. int32 | int32 collapses to int32, so it takes part in arithmetic like any other integer:

let single: int32 | int32 = 20;
PrintLine("single + 1         {}", single + 1);
WrittenIs the type
int32 | boolint32 | bool
bool | int32the same type
int32 | bool | int32the same type
int32 | int32plain int32

That collapse looks like a curiosity now. It matters in Generic sum, where T | U is written before anyone knows whether T and U are the same type.

Comparing sums

Two sums are equal when the same member is active and the values agree:

let off: int32 | bool = false;
let zero: int32 | bool = 0;
PrintLine("limit == 250       {}", limit == 250);
PrintLine("repeated == limit  {}", repeated == limit);
PrintLine("off == zero        {}", off == zero);

off == zero is false. A switch and a number are never equal, whatever their bits look like — false and 0 may well be the same byte in memory, but the sum remembers that one is a bool and the other an int32.

Sum or variant?

Both hold one of several things. The difference is what the alternatives are called:

VariantSum
Declared asvariant Reading { … }int32 | bool, no declaration
Alternatives arenamed cases, such as .Exacttypes, such as int32
Two of the same typeallowed, as two different casesimpossible — duplicates collapse
Matched with.Exact(value) =>value: int32 =>

Reach for a variant when the cases mean different things even if they carry the same type — a temperature in Celsius and one in Fahrenheit are both float64. Reach for a sum when the types themselves are the difference, as with a setting that is a number, a switch or a word.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A sum type holds one value whose type is one of a short list. `int32 | bool` is "an int32 or
// a bool": a variable of that type can hold 8080 now and `true` later, and it always knows which
// of the two it is holding.
//
// You have met this shape before. A variant also holds one of several things, but it names its
// cases. A sum names only types, so its list is a *set* of types: the order they are written in
// does not matter, and a type written twice counts once. `int32 | bool`, `bool | int32` and
// `int32 | bool | int32` are three spellings of one type.
//
// Getting a member back out takes a pattern, and that is the next lesson. Here a sum is only
// built, stored, reassigned and compared.
import Io::PrintLine;

// A setting in a configuration file is a number, a switch or a word. A type alias gives the sum
// a short name; it is still exactly `int32 | bool | char8[..]`.
type Setting = int32 | bool | char8[..];

func Main() -> int {
    // A value of any member type goes straight into the sum. The value picks the member: 8080
    // is an integer, and the sum has exactly one integer member to put it in.
    let port: Setting = 8080;
    let verbose: Setting = true;
    let name: Setting = "server";

    // A `var` sum can switch members. Each assignment replaces both the value and the record of
    // which member is active.
    var limit: int32 | bool = 100;
    limit = false;
    limit = 250;

    // Order and duplicates do not make a new type, so these copies need no conversion at all.
    let reordered: bool | int32 = limit;
    let repeated: int32 | bool | int32 = reordered;

    // Two sums are equal when the same member is active and the values agree. A number and a
    // switch are never equal, whatever their bits look like.
    let off: int32 | bool = false;
    let zero: int32 | bool = 0;
    PrintLine("limit == 250       {}", limit == 250);
    PrintLine("repeated == limit  {}", repeated == limit);
    PrintLine("off == zero        {}", off == zero);

    // A sum of one type is just that type: `int32 | int32` collapses to `int32`, so it takes
    // part in arithmetic like any other integer.
    let single: int32 | int32 = 20;
    PrintLine("single + 1         {}", single + 1);

    // The other settings are stored and ready, but printing one needs its member first.
    // `PrintLine("{}", port)` is rejected: a sum is not a number, even when it holds one.
    return 0;
}

Run it

cd Examples/SumTypes/SumType
rux run
limit == 250       true
repeated == limit  true
off == zero        false
single + 1         21

Common mistakes

Using a sum as if it were its member.
A sum is not a number, even while it holds one. PrintLine("{}", port) fails with error: argument 2 to 'PrintLine' has type 'bool8 | char8[..] | int32', but variadic parameter 'args' requires 'Display', and with let x: int32 | bool = 3; the sum x + 1 fails with error: operator '+' cannot combine left operand 'bool8 | int32' with right operand 'int'. Take the member out with a typed pattern first.
A literal that fits more than one member.
let s: int32 | int64 = 5; fails with error: integer literal is ambiguous for 'int32 | int64', which has several integer members; add a suffix or a cast. The compiler will not guess which integer you meant. Write 5i64, or 5 as int32.
A value whose type is not a member.
let s: int32 | bool = 2.5; fails with error: cannot assign 'float64' to 'bool8 | int32'. A sum holds its members and nothing else — no conversion is tried.
Comparing a sum with a plain value.
limit == false fails with error: operator '==' cannot compare 'bool8 | int32' with 'bool8', and the compiler's note explains why: a comparison never injects, widens, or wraps an operand. Give the value the sum's type first, as the program does with let off: int32 | bool = false;, and compare two sums.

Try it yourself

  1. Add float64 to Setting and store let ratio: Setting = 0.75;. Why is 0.75 not ambiguous, when an integer literal would be for int32 | int64?
  2. Declare let wide: int32 | int64 = 5;, read the error, then fix it with a suffix.
  3. Declare let alsoOff: bool | int32 = false; and print off == alsoOff. The two types are spelled differently — can they be compared?
  4. Try PrintLine("{}", port); and read which parameter type the sum fails to satisfy.

Learn more