Tuples

A tuple groups a fixed number of values of possibly different types. You do not need to give a tuple a name — use it directly where you need to pass or return multiple values together. Access individual elements with .0, .1, etc.

Creating tuples

Write the elements in parentheses, separated by commas. The compiler infers the element types from what you put in. You can add an explicit type annotation when needed.

Elements of different types

The elements do not have to share a type — this is the usual reason to reach for a tuple instead of a vector.

Accessing elements

Use .0, .1, .2, ... to read individual elements. The index is part of the program text, so it is checked when you compile: naming an element the tuple does not have is an error, never a null.

Modifying elements

Tuple elements can be reassigned like ordinary variables.

Tuples as function parameters

Pass a tuple to a function by value (a copy) or by reference. A value parameter gets its own copy — changes inside the function do not affect the caller. A reference parameter (&) gives the function a direct link to the caller's tuple so it can modify individual elements in place, text elements included.

Returning tuples

A function can return a tuple to give back more than one value at once.

Destructuring

Assign a tuple to multiple names in one step using the (a, b) = expr form. This is concise when a function returns a tuple and you need both values.

Comparing tuples

Two tuples of the same shape compare element by element, left to right.

Three or more elements

Tuples can have any number of elements.

fn min_max(lo: integer, hi: integer) -> (integer, integer) {
  if lo <= hi {
    (lo, hi)
  } else {
    (hi, lo)
  }
}
fn swap_ints(pair: &(integer, integer)) {
  tmp = pair.0;
  pair.0 = pair.1;
  pair.1 = tmp;
}

Writes to its own copy. The caller's tuple is a different tuple, so the write below is invisible outside this function.

fn doubled_first(p: (integer, integer)) -> integer {
  p.0 = p.0 * 2;
  p.0 + p.1
}

A tuple is the natural return type when the two values have different types.

fn label_and_score(n: integer) -> (text, integer) {
  if n >= 50 { ("pass", n) } else { ("fail", n) }
}
fn main() {

Creating tuples

  t = (10, 20);
  assert(t.0 == 10, "t.0: {t.0}");
  assert(t.1 == 20, "t.1: {t.1}");

Type annotation

  w: (integer, integer) = (3, 4);
  assert(w.0 + w.1 == 7, "annotated sum: {w.0+w.1}");

Elements of different types

Each element keeps its own type, and .0 / .1 read them as that type.

  entry = (1, "one");
  assert(entry.0 == 1, "entry.0: {entry.0}");
  assert(entry.1 == "one", "entry.1: {entry.1}");
  mixed: (text, float, boolean) = ("pi", 3.5, true);
  assert(mixed.0 == "pi", "mixed.0: {mixed.0}");
  assert(mixed.1 == 3.5, "mixed.1: {mixed.1}");
  assert(mixed.2, "mixed.2: {mixed.2}");

A tuple element may itself be a tuple.

  nested = (1, (2, 3));
  assert(nested.1.0 == 2 && nested.1.1 == 3, "nested: {nested.1.0},{nested.1.1}");

Modifying elements

  v = (1, 2);
  v.0 = 10;
  assert(v.0 + v.1 == 12, "after assign: {v.0}+{v.1}");

Tuples as function parameters

doubled_first writes to its parameter and returns what it computed. The caller's tuple still reads 10 — the parameter was a copy of it.

  p = (10, 20);
  inner = doubled_first(p);
  assert(inner == 40, "the function saw its own doubled copy: {inner}");
  assert(p.0 == 10, "and the caller's tuple is untouched: {p.0}");

Tuples as reference parameters (swap in place)

swap_ints exchanges the caller's two elements through its & parameter.

  pair = (3, 7);
  swap_ints(pair);
  assert(pair.0 == 7 && pair.1 == 3, "after swap: ({pair.0},{pair.1})");

Returning a tuple from a function

  bounds = min_max(5, 2);
  assert(bounds.0 == 2, "min: {bounds.0}");
  assert(bounds.1 == 5, "max: {bounds.1}");

Already in order

  bounds2 = min_max(1, 9);
  assert(bounds2.0 == 1, "min2: {bounds2.0}");
  assert(bounds2.1 == 9, "max2: {bounds2.1}");

A returned tuple with elements of different types works the same way.

  graded = label_and_score(72);
  assert(graded.0 == "pass" && graded.1 == 72, "graded: {graded.0},{graded.1}");

Destructuring

  (lo, hi) = min_max(8, 3);
  assert(lo == 3, "destructured lo: {lo}");
  assert(hi == 8, "destructured hi: {hi}");

Destructuring works for mixed types too, and for more than two names.

  (name, score) = label_and_score(20);
  assert(name == "fail" && score == 20, "destructured mixed: {name},{score}");

Comparing tuples

The first element decides; later elements are consulted only while the earlier ones are equal. Text elements compare by value.

  assert((1, 9) < (2, 0), "the first element decides");
  assert((1, 9) < (1, 10), "it ties, so the second decides");
  assert(!((1, 9) < (1, 9)), "identical is not strictly less");
  assert((1, 9) <= (1, 9), "but it is less-or-equal");
  assert((1, "abc") == (1, "abc"), "text compares by value, not identity");
  assert((1, "abc") != (1, "abd"), "and differing text is not equal");

Three or more elements

  u = (1, 2, 3);
  assert(u.0 + u.1 + u.2 == 6, "triple sum: {u.0}+{u.1}+{u.2}");
  triple = (10, 20, 30);
  triple.1 = 99;
  assert(triple.0 == 10 && triple.1 == 99 && triple.2 == 30,
    "triple modified: ({triple.0},{triple.1},{triple.2})");
  wide = (1, 2, 3, 4, 5, 6, 7, 8);
  assert(wide.7 == 8, "eight elements, last one reachable: {wide.7}");
}