Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tuples and multiple return values

A tuple groups a fixed number of values, each of its own type, into one value. Its main use is returning several values from a function:

# A function returns several values as a tuple.
divide(a: int, b: int): (int, int) {
    return (a / b, a % b);
}

# Elements can be of any type, and a tuple can hold another tuple.
describe(n: int): (String, (bool, int)) {
    return ("number", (n % 2 == 0, n * n));
}

main(): int {
    # Read elements by position: t.0, t.1, ...
    var t = divide(17, 5);
    assert(t.0 == 3, "the quotient");
    assert(t.1 == 2, "the remainder");

    # Nested tuples chain: d.1.0 is element 0 of element 1.
    var d = describe(4);
    assert(d.0 == "number", "the name");
    assert(d.1.0 && d.1.1 == 16, "even, and its square");

    # A tuple type can be written out; the literal converts to it.
    var wide: (long, double) = (1, 2);
    assert(wide.0 == 1 && wide.1 == 2.0, "converted elements");
    return 0;
}

Tuple types and values

  • A tuple type lists its element types in parentheses: (int, String), (bool, (int, int)). A tuple has at least two elements.
  • A tuple value lists its elements the same way: (a / b, a % b). The compiler infers its type from the elements, so (1, "x") is an (int, String).
  • Where a tuple type is expected (a declared variable, a parameter, a return value), each element converts to its element type, as it would on its own: var wide: (long, double) = (1, 2);.
  • t.0, t.1, … read an element by position, counting from 0. t.1.0 is element 0 of element 1. An element that does not exist is a compile error.
  • Elements are read-only. t.0 = 3 is a compile error; build a new tuple instead: t = (3, t.1);.
  • Tuple<A, B> is another spelling of (A, B), for places where a type in angle brackets reads better.
  • (int, String)? is an optional tuple, Optional<(int, String)> (see Optional values).

Labels

A tuple type can name its elements: (line: int, column: int), and so can a tuple value: (line: 3, column: 7). A label is another name for a position, so p.line and p.0 read the same element:

# Labels name a tuple's elements.
position(): (line: int, column: int) {
    return (line: 3, column: 7);
}

struct Token {
    var text: String = "";
    var at: (line: int, column: int);
}

main(): int {
    var p = position();
    assert(p.line == 3 && p.column == 7, "read by label");
    assert(p.0 == 3, "positions still work");

    # Labeled and unlabeled tuples convert to each other.
    var plain: (int, int) = p;
    assert(plain.1 == 7, "same elements");

    # A labeled value carries its labels into the variable's type.
    var end = (line: 4, column: 1);
    assert(end.line == p.line + 1, "labeled literal");

    var t = Token();
    t.at = (5, 6);
    assert(t.at.column == 6, "a labeled field");
    return 0;
}
  • Labels belong to the type you write, not to the value. A labeled tuple and an unlabeled one with the same element types are the same type, so each converts to the other freely.
  • Either every element has a label or none does, and labels in one tuple must differ.
  • Labels do not pass through a type argument: Optional<(line: int, column: int)> is the same type as Optional<(int, int)>, so its value() returns an unlabeled tuple. Declare a labeled variable to name the elements again.
  • var p = (line: 3, column: 7); gives p the labeled type (line: int, column: int). A labeled value takes at least two elements.
  • A labeled value initializing or returned as a labeled type must use its labels in order: (column: 7, line: 3) where a (line: int, column: int) is expected is an error, not a swap. An unlabeled value, or a labeled one going to an unlabeled type, converts by position.
  • A destructuring assignment lists places alone: (a, b) = (b, a);, never (x: a, y: b) = ....

Destructuring

var (a, b) = value; declares one variable for each element of a tuple:

struct Point {
    var x: int = 0;
    var y: int = 0;
}

min_max(values: Array<int>): (int, int) {
    var low = values[0];
    var high = values[0];
    for (var v in values) {
        if (v < low) {
            low = v;
        }
        if (v > high) {
            high = v;
        }
    }
    return (low, high);
}

main(): int {
    var values = Array<int>();
    values.append(4);
    values.append(-2);
    values.append(9);

    # Destructure the result into new variables.
    var (low, high) = min_max(values);
    assert(low == -2 && high == 9, "both ends");

    # `_` skips an element; a binding may state its type.
    var (_, top: long) = min_max(values);
    assert(top == 9, "only the maximum");

    # `const` makes every binding constant.
    const (first, last) = (values[0], values[2]);
    assert(first == 4 && last == 9, "constants");

    # Assign to existing places. The right side is evaluated first,
    # so this swaps.
    var a = 1;
    var b = 2;
    (a, b) = (b, a);
    assert(a == 2 && b == 1, "swapped");

    var p = Point();
    (p.x, p.y) = min_max(values);
    assert(p.x == -2 && p.y == 9, "fields are places too");
    return 0;
}
  • The number of names must match the number of elements.
  • _ skips an element.
  • A name may state its type, var (_, top: long) = …; the element converts to it. Without one, the variable takes the element’s type.
  • const (a, b) = … declares constants.
  • Patterns nest: var (id, (name, age)) = … takes an element that is itself a tuple apart.
  • A for loop destructures each element: for (var (name, count) in pairs).
  • (a, b) = value; assigns to places that already exist: variables, fields (p.x) and array elements. The whole right side is evaluated before any assignment, so (a, b) = (b, a) swaps.

Tuples and references

A tuple is a value, like a number. It is not an object on the heap, and copying it copies its elements. An element that is a reference is shared, not copied, exactly as if you had assigned it to a variable:

  • Building a tuple from a variable adds a reference to the object; building it from a function result takes over that result’s reference.
  • A tuple holds its references until it goes out of scope or is replaced, then releases them.
  • Destructuring moves each element into its variable. After var (a, b) = pair();, a holds the only reference to its object, just as it would after var a = make();.

A tuple is thread-safe when all its elements are, so a module-scope const origin: (int, long) = (0, 0); is allowed.

Collections, comparing and printing

A tuple works anywhere a type does, including as a type argument: Array<(int, String)>, HashMap<(int, int), String>. Iterating a HashMap yields (key, value) tuples:

main(): int {
    # A tuple can be a key: here, a grid position.
    var grid = HashMap<(int, int), String>();
    grid[(0, 0)] = "start";
    grid[(2, 3)] = "goal";
    assert(grid[(2, 3)] == "goal", "looked up by value");

    # Tuples compare element by element.
    assert((1, "a") == (1, "a"), "equal");
    assert((1, "a") != (1, "b"), "different");

    # A HashMap yields (key, value) tuples; destructure them in the loop.
    var ages = HashMap<String, int>();
    ages["ada"] = 36;
    ages["grace"] = 85;
    var total = 0;
    for (var (name, age) in ages) {
        total += age;
    }
    assert(total == 121, "every entry");

    # An array of tuples.
    var pairs = Array<(String, int)>();
    pairs.append(("x", 1));
    pairs.append(("y", 2));
    for (var (name, count) in pairs) {
        print($"{name}: {count}");
    }
    return 0;
}
  • == and != compare element by element. Each element compares the way it would on its own: a type with an == operator (such as String) uses it, and any other struct compares by identity (the same object).
  • A tuple can be a HashMap key or a HashSet member. Its hash combines the hashes of its elements, so equal tuples hash equally.
  • to_string((1, "two")) is (1, two), so a tuple of up to eight values that each have a to_string can be interpolated and printed: print($"{t}").
  • copy(t) (see Copying a value) copies each element: a struct element as copy would copy it on its own, anything thread-safe as is.

Matching tuples

A match over a tuple tests all of its elements at once. A tuple pattern lists a pattern for each element: (Rock, Scissors), (Number(v), 1), (4, column). Write the tuple in the match itself with double parentheses, match ((a, b)), or match a tuple value, match (at).

# `match` takes a tuple apart, testing every element at once.
enum Move {
    Rock,
    Paper,
    Scissors,
}

# Who wins: 1 for the first player, 2 for the second, 0 for a draw.
winner(first: Move, second: Move): int {
    match ((first, second)) {
        (Rock, Scissors), (Paper, Rock), (Scissors, Paper) => {
            return 1;
        }
        (Rock, Rock), (Paper, Paper), (Scissors, Scissors) => {
            return 0;
        }
        else => {
            return 2;
        }
    }
}

enum Token {
    Number(value: int),
    Word(text: String),
}

# Literals, cases with values, bound names and `_` mix in one pattern.
describe(token: Token, line: int): String {
    return match ((token, line)) {
        (Number(v), 1) => "a number on the first line",
        (Word(text), _) => text,
        (_, _) => "another number",
    };
}

main(): int {
    assert(winner(Move.Rock, Move.Scissors) == 1, "rock blunts scissors");
    assert(winner(Move.Paper, Move.Scissors) == 2, "scissors cut paper");
    assert(winner(Move.Paper, Move.Paper) == 0, "a draw");

    assert(describe(Token.Word("hi"), 3) == "hi", "a word binds its text");
    assert(describe(Token.Number(7), 1) == "a number on the first line", "a literal element");

    # A name binds its element; `is` tests one pattern.
    var at = (4, 2);
    if (at is (4, column)) {
        assert(column == 2, "bound by the pattern");
    }
    return 0;
}
  • Each element takes a case of its enum (Rock, Word(text)), a literal (1, "x", true), a range or a constant (0..9, LIMIT), another tuple pattern, _ for any value, or a name.
  • A name binds its element, unless it names a module-scope constant, which the element is compared with, or the element is an enum with a case of that name, which it then matches. So in (Circle, other) over two shapes, Circle is a case and other holds the second shape.
  • An arm’s patterns bind the same names, each at the same element: (x, 0), (0, x) is an error, since x would mean a different element depending on which pattern matched.
  • The arms must handle every value unless there is an else. A missing value is named with an example: match on '(Shape, int)' does not handle '(Line, _)'. Only enum cases and true/false can be listed in full; other literals and ranges need a pattern with _ or a name at that element.
  • A pattern that the arms before it already cover is an error, and an else after arms that cover everything is a warning.
  • value is (4, column) tests one tuple pattern, and binds its names as the whole condition of an if or while, as for enums.