Patterns · Lesson 7.3

Tuple pattern

Source
Decide on several values at once by matching a tuple, with arms such as (0, 0) =>, (x, 0) => and (_, y) =>.
You'll need: Tuple, Guard, Range pattern

A match looks at one value. Some decisions depend on two: where a point lies depends on both of its coordinates, and FizzBuzz on two remainders at once. Put the values in a tuple and match the tuple, and every arm can talk about all of them together.

One part per member

Each arm is then a tuple pattern with one part per member, and the arm is taken only when every part matches its member:

PatternMatches when
(0, 0)both members are 0
(x, 0)the second member is 0; the first is bound to x
(_, y)any first member, ignored; the second is bound to y

A part can be any pattern you have met so far: a literal, a name, _ or a range. Literal parts test a member, names bind one, and _ ignores one.

Where a point lies

func Locate(point: (int32, int32)) {
    match point {
        (0, 0) => PrintLine("the origin"),
        (x, 0) => PrintLine("on the x axis, {} across", x),
        (0, y) => PrintLine("on the y axis, {} up", y),
        (x, y) if x == y => PrintLine("on the diagonal, {} each way", x),
        (_, y) if y > 0 => PrintLine("above the x axis, {} up", y),
        else => PrintLine("below the x axis")
    }
}

Arms are tried from top to bottom, so the most specific ones go first. (0, 0) would also fit (x, 0) and (0, y), which is why it comes before both. A guard follows the pattern as usual and can compare the members with each other: (x, y) if x == y is something no literal pattern can say.

A pair of int32s has far more combinations than any list of arms, so this match ends with else, which decides what all the others mean.

Building the tuple in the match

The tuple need not exist beforehand. Build it right in the match from the values the decision depends on — here, FizzBuzz decided by both remainders at once:

match (n % 3, n % 5) {
    (0, 0) => Print("FizzBuzz"),
    (0, _) => Print("Fizz"),
    (_, 0) => Print("Buzz"),
    else => Print("{}", n)
}

Compare it with the if / else if version from the FizzBuzz project: the four outcomes are now four lines that read like a truth table.

Covering every combination

Two bools make only four combinations, so the arms can list them all and need no else:

let advice = match (raining, windy) {
    (true, true) => "a raincoat",
    (true, false) => "an umbrella",
    (false, true) => "a jacket",
    (false, false) => "nothing extra"
};

The compiler checks the list. Leave one combination out and it names the missing one exactly.

Structs work by name

A struct can be taken apart the same way, by field name instead of position: Point { x: 0, y } => tests x and binds y. A field written alone, like y, is short for y: y. The next lesson, Struct pattern, uses this form on variant cases.

The program

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

Src/Main.rux
// A `match` looks at one value. To decide on two values together, put them in a tuple and match
// the tuple. Each arm is then a tuple pattern with one part per member, and the arm is taken only
// when every part matches its member:
//
//     (0, 0)    both members are 0
//     (x, 0)    the second member is 0, and the first is bound to `x`
//     (_, y)    any first member, ignored, and the second bound to `y`
//
// A part can be any pattern you have met so far: a literal, a name, `_` or a range. Arms are tried
// from top to bottom, so the most specific ones go first, and a guard can follow the pattern as
// usual. A pair of numbers has far more combinations than any list of arms, so a match on one ends
// with `else`, which decides what all the others mean. A pair of `bool`s has only four, so its
// arms can list them all, and the compiler checks that none is missing.
import Io::{ Print, PrintLine };

// Where a point lies. Literal parts test a member, names bind one, and `_` ignores one.
func Locate(point: (int32, int32)) {
    match point {
        (0, 0) => PrintLine("the origin"),
        (x, 0) => PrintLine("on the x axis, {} across", x),
        (0, y) => PrintLine("on the y axis, {} up", y),
        (x, y) if x == y => PrintLine("on the diagonal, {} each way", x),
        (_, y) if y > 0 => PrintLine("above the x axis, {} up", y),
        else => PrintLine("below the x axis")
    }
}

func Main() -> int {
    let points: (int32, int32)[6] = [(0, 0), (4, 0), (0, 6), (3, 3), (-1, 5), (2, -7)];
    for point in points {
        Print("({}, {}) is ", point.0, point.1);
        Locate(point);
    }

    // The tuple need not exist beforehand: build it right in the `match` from the values the
    // decision depends on. This is FizzBuzz, decided by both remainders at once.
    for n in 1..=15 {
        match (n % 3, n % 5) {
            (0, 0) => Print("FizzBuzz"),
            (0, _) => Print("Fizz"),
            (_, 0) => Print("Buzz"),
            else => Print("{}", n)
        }
        if n < 15 {
            Print(" ");
        }
    }
    PrintLine("");

    // Two `bool`s make four combinations, and these arms list them all, so no `else` is needed.
    // Leave one out and the compiler names it:
    //     error: match on '(bool8, bool8)' is not exhaustive; missing (true, false)
    let raining = true;
    let windy = false;
    let advice = match (raining, windy) {
        (true, true) => "a raincoat",
        (true, false) => "an umbrella",
        (false, true) => "a jacket",
        (false, false) => "nothing extra"
    };
    PrintLine("rain without wind: take {}", advice);

    // A struct can be taken apart the same way, by field name instead of position:
    // `Point { x: 0, y } =>` tests `x` and binds `y`. A field written alone, like `y`, is short
    // for `y: y`.
    return 0;
}

Run it

cd Examples/Patterns/TuplePattern
rux run
(0, 0) is the origin
(4, 0) is on the x axis, 4 across
(0, 6) is on the y axis, 6 up
(3, 3) is on the diagonal, 3 each way
(-1, 5) is above the x axis, 5 up
(2, -7) is below the x axis
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
rain without wind: take an umbrella

Common mistakes

A missing combination.
Leave (true, false) out of the weather match and the compiler stops with error: match on '(bool8, bool8)' is not exhaustive; missing (true, false). Either add the arm or, if the remaining combinations really share an answer, an else.
The wrong number of parts.
A pattern needs exactly one part per member. Matching a pair with (0, 0, 0) => fails with error: tuple pattern has 3 elements, but matched tuple has 2.
A general arm above a specific one.
(x, 0) also fits (0, 0). Put it first and the origin is reported as "on the x axis, 0 across" — the compiler accepts the order, so the arms must be arranged from most to least specific by you.

Try it yourself

  1. Add an arm to Locate for points on the other diagonal, where x == -y. Where does it have to go to be reached by (-2, 2)?
  2. Rewrite FizzBuzz to also print "Bazz" for multiples of 7, matching a three-member tuple (n % 3, n % 5, n % 7).
  3. Match a (bool, int32) pair — say, (member, age) — using a range for the age, such as (true, 0..=17) =>.

Learn more