Sum types · Lesson 10.2

Typed pattern

Source
Match a sum by the type it holds — n: int32 => — and use the member as an ordinary value.

A sum knows which member it holds, and a typed pattern asks it. The arm n: int32 => matches only when the active member is int32, and binds that value to n as an int32 — so inside the arm it is an ordinary number again. It prints, it adds, it compares.

In the last lesson a sum could be built, stored and compared, but not printed or used in arithmetic. This lesson is the way back out.

One arm per member

A variant match arm names a case: .Exact(value) =>. A sum has no case names, so its arm names a type instead — the binding, a colon, the member type:

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

func Show(key: char8[..], value: Setting) {
    match value {
        seconds: int32 => PrintLine("{:8} = {} ms", key, seconds * 1000),
        flag: bool => PrintLine("{:8} = {}", key, flag ? "on" : "off"),
        word: char8[..] => PrintLine("{:8} = \"{}\"", key, word)
    }
}

Each binding has its member's own type. seconds is an int32, so seconds * 1000 is integer arithmetic. flag is a bool, so it can drive the ternary. word is a text slice, ready for a placeholder.

flowchart LR
    v["value: Setting"] --> q{"Which member<br/>is active?"}
    q -- "int32" --> a1["seconds: int32 =><br/>seconds * 1000"]
    q -- "bool" --> a2["flag: bool =><br/>on or off"]
    q -- "char8[..]" --> a3["word: char8[..] =><br/>print the word"]

Exactly one arm runs, the one whose type is the active member. The order of the arms does not change which one that is — a member can only ever match its own arm.

A match that produces a value

As an expression, a match on a sum produces one value from whichever arm ran, just as a match expression on a number does:

func Weight(value: Setting) -> int32 {
    return match value {
        n: int32 => n,
        flag: bool => flag ? 1 : 0,
        _: char8[..] => -1
    };
}

All three arms produce an int32, so the whole match is an int32, whichever member came in.

Matching without a binding

The text arm in Weight needs no value from its member — only the fact that the setting is text. _: char8[..] matches the type without naming it. Use it whenever an arm would otherwise bind a name it never reads.

PatternMatches when the active member isBinds
n: int32int32n, an int32
_: char8[..]char8[..]nothing
elseanything earlier arms leftnothing

Every member must be covered

A match on a sum must handle every member, just as a variant match handles every case. Leave out an arm and the compiler names the pattern you still need. Without the int32 arm in Show:

error: match on 'bool8 | char8[..] | int32' is not exhaustive; missing _: int32

The compiler spells the sum in its own sorted order, and bool by its full name bool8, so the members may not appear in the order you wrote them.

When several members are handled alike, an else arm covers whatever the earlier arms left:

match value {
    n: int32 => PrintLine("number {}", n),
    else => PrintLine("something else")
}

That works, but it gives something up: add a fourth member to Setting later and this match quietly sends it to else. With one arm per member, the compiler stops you and points at every match that needs the new arm. The next lesson shows a middle way — one arm for a chosen group of members.

The program

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

Src/Main.rux
// A sum knows which member it holds, and a typed pattern asks it. The arm `n: int32 =>` matches
// only when the active member is `int32`, and binds that value to `n` *as an int32*, so inside
// the arm it is an ordinary number again: it prints, it adds, it compares.
//
// A variant arm names a case, `.Exact(value) =>`. A sum has no case names, so its arm names a
// type instead: the binding, a colon, the member type. `_: char8[..] =>` matches the member
// without binding it, for when only the fact matters.
//
// The match must cover every member, just as a variant match covers every case. Leave out the
// `int32` arm below and the compiler names the pattern you still need:
//     error: match on 'bool8 | char8[..] | int32' is not exhaustive; missing _: int32
// (The compiler spells the sum in its own sorted order, and `bool` by its full name `bool8`.)
import Io::PrintLine;

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

// One arm per member. Each binding has the member's own type, so `seconds * 1000` is integer
// arithmetic and `word` is a text slice ready for a placeholder.
func Show(key: char8[..], value: Setting) {
    match value {
        seconds: int32 => PrintLine("{:8} = {} ms", key, seconds * 1000),
        flag: bool => PrintLine("{:8} = {}", key, flag ? "on" : "off"),
        word: char8[..] => PrintLine("{:8} = \"{}\"", key, word)
    }
}

// As an expression, a match on a sum produces one value from whichever arm ran. Here the text
// arm needs no value from its member, so `_` matches the type without naming it.
func Weight(value: Setting) -> int32 {
    return match value {
        n: int32 => n,
        flag: bool => flag ? 1 : 0,
        _: char8[..] => -1
    };
}

func Main() -> int {
    let timeout: Setting = 30;
    let verbose: Setting = true;
    let mode: Setting = "fast";

    Show("timeout", timeout);
    Show("verbose", verbose);
    Show("mode", mode);

    PrintLine("weights  {} {} {}", Weight(timeout), Weight(verbose), Weight(mode));
    return 0;
}

Run it

cd Examples/SumTypes/TypedPattern
rux run
timeout  = 30000 ms
verbose  = on
mode     = "fast"
weights  30 1 -1

Common mistakes

Leaving out a member.
A match on a sum must be exhaustive. Drop the int32 arm from Show and it fails with error: match on 'bool8 | char8[..] | int32' is not exhaustive; missing _: int32.
Naming a type that is not a member.
n: int64 => on a Setting fails with error: type 'int64' is not a member or subset of sum 'bool8 | char8[..] | int32'. The pattern must name the member exactly — an int32 member is not matched by a wider integer type.
Writing only the type.
An arm int32 => "number" fails with error: pattern 'int32' cannot bind a new variable because 'int32' already names a type. A bare name in a pattern is a new binding, and a type name cannot be one. Write _: int32 =>, or n: int32 => if you need the value.

Try it yourself

  1. Add float64 to Setting and run the program. Read the error, then add a float64 arm to both Show and Weight.
  2. Write func Kind(value: Setting) -> char8[..] that returns "number", "switch" or "text". Which patterns need no binding at all?
  3. Rewrite Show with one int32 arm and an else arm. Then add a fourth member to Setting and compare: which version of Show makes the compiler complain?

Learn more