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.0is element 0 of element 1. An element that does not exist is a compile error.- Elements are read-only.
t.0 = 3is 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 asOptional<(int, int)>, so itsvalue()returns an unlabeled tuple. Declare a labeled variable to name the elements again. var p = (line: 3, column: 7);givespthe 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
forloop 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();,aholds the only reference to its object, just as it would aftervar 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 asString) uses it, and any other struct compares by identity (the same object).- A tuple can be a
HashMapkey or aHashSetmember. 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 ato_stringcan be interpolated and printed:print($"{t}").copy(t)(see Copying a value) copies each element: a struct element ascopywould 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,Circleis a case andotherholds the second shape. - An arm’s patterns bind the same names, each at the same element:
(x, 0), (0, x)is an error, sincexwould 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 andtrue/falsecan 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
elseafter arms that cover everything is a warning. value is (4, column)tests one tuple pattern, and binds its names as the whole condition of aniforwhile, as for enums.