A tuple is a fixed-size, heterogeneous product with positional fields. The type and the value both use parentheses and commas.
fn f(): int {
entry: (string, int, bool) = ("port", 8080, true)
if entry.2 {
return entry.1
}
return 0
}Tuple identity is structural and ordered: (int, string) and
(string, int) are different types, as are tuples of different arity.
fn f(): int {
t: (int, int) = (1, 2, 3)
return t.0
}Indexing
.0, .1, .2 … select an element. The index is syntax checked at compile
time, not a runtime lookup.
fn f(): int {
entry := ("port", 8080, true)
name := entry.0
port := entry.1
return port + name.len()
}An out-of-range index is rejected:
fn f(): int {
entry := ("a", 1)
return entry.5
}There is no dynamic tuple indexing — entry[i] is not tuple access. When the
position is chosen at runtime you want a list or a fixed array, not a tuple.
Destructuring
A tuple pattern binds every position at once. In binding and iteration positions the outer parentheses are optional.
fn f(): int {
(host, port) := ("localhost", 8080)
user, id := ("ada", 7)
return port + host.len() + id + user.len()
}_ ignores one position — exactly one, never a variable number:
fn f(): int {
(first, _, third) := (1, 2, 3)
return first + third
}Tuple patterns work in match arms too, and compose with literal patterns and
guards:
fn quadrant(p: (int, int)): string {
return match p {
(0, 0) => "origin"
(x, y) if x > 0 && y > 0 => "north-east"
(x, _) if x < 0 => "west"
_ => "elsewhere"
}
}
fn f(): string { return quadrant((3, 4)) }Note that assignment cannot be destructured: a, b = b, a is rejected because
, on the left of = is not an assignment target. Use := to bind new names.
Multi-value returns
Tuples give a function several results without a special calling convention. The tuple is the return type.
fn divmod(value: int, divisor: int): (int, int) {
return (value / divisor, value % divisor)
}
fn f(): string {
quotient, remainder := divmod(17, 5)
return "${quotient} r ${remainder}"
}Parentheses may be dropped in a return when the commas clearly belong to that
expression:
fn pair(): (int, string) {
return 1, "one"
}
fn f(): int { return pair().0 }Keep them wherever a call or an operator could claim the comma. In particular,
call arguments are not a tuple: a function that takes one (int, string)
needs an explicit parenthesized argument.
fn take(p: (int, string)): int { return p.0 }
fn f(): int { return take(1, "a") }Tuple returns combine with ? and catch like any other value:
error ParseError { Bad }
fn parse_endpoint(s: string): (string, int) ! ParseError {
parts := s.split(":")
if parts.len() != 2 { error Bad }
host := parts.get(0) ?? ""
port := (parts.get(1) ?? "").to_int() ?? 0
return (host, port)
}
fn render(s: string): string {
host, port := parse_endpoint(s) catch {
Bad => return "malformed"
}
return "${host} on port ${port}"
}
fn f(): string { return render("localhost:8080") }Once a result grows past two or three positions, or once the positions stop being obvious, switch to a record or a struct so the names travel with the values.
Tuples in collections
A [](A, B) iterates as tuples, and the loop pattern can destructure them
directly.
fn f(): int {
rows: [](int, string) = [(1, "a"), (2, "bb"), (3, "ccc")]
mut score := 0
for id, label in rows {
score += id * label.len()
}
return score
}The same works for maps, whose iteration yields key/value pairs:
fn total(prices: Map[string, int]): int {
mut sum := 0
for name, price in prices {
sum += price + name.len()
}
return sum
}
fn f(): int {
mut prices: Map[string, int] = Map.new()
prices.put("apple", 3)
prices.put("pear", 5)
return total(prices)
}Tuples nest inside generics anywhere a type can go:
fn f(): int {
mut spans: Map[string, (int, bool)] = Map.new()
spans.put("body", (12, true))
entry := spans.get("body") ?? (0, false)
return entry.0
}Tuples of comparable elements are themselves comparable and equatable, which makes them convenient sort keys:
fn f(): int {
xs := [(3, "c"), (1, "a"), (2, "b")]
sorted := xs.sorted()
return (sorted.get(0) ?? (0, "")).0
}fn f(): bool {
return (1, 2) < (1, 3) && (1, "a") == (1, "a")
}Tuple fields and options
A tuple can be a struct field or an optional value like anything else:
struct Span {
range: (int, int)
label: string
}
fn width(s: Span): int {
return s.range.1 - s.range.0
}
fn f(): int {
return width(Span { range: (2, 7), label: "header" })
}fn f(): int {
maybe: (string, int)? = ("a", 1)
t := maybe ?? ("", 0)
return t.1
}Element coercion
An expected tuple type constrains its elements, but not every scalar coercion is
applied per element. An int literal does not widen into a float position:
fn f(): float {
t: (float, string?) = (1, "one")
return t.0
}Write the element in its target type:
fn f(): float {
t: (float, string?) = (1.0, "one")
return t.0
}Lifting a bare value into an optional element, as with "one" into string?
above, does work.
Unit
() is the unit value — the result of an expression with nothing useful to
report.
fn f(): int {
ignored := ()
return 0
}Function signatures spell “no result” as void (or Unit), or omit the return
type entirely. A one-element tuple (x,) is legal but rarely what you want;
parentheses around a single expression with no comma are grouping, not a tuple.
A composed example
A word-frequency pass that uses tuples for the pair produced by each stage and a record only where names would help.
fn tally(words: []string): Map[string, int] {
mut counts: Map[string, int] = Map.new()
for w in words {
counts.put(w, (counts.get(w) ?? 0) + 1)
}
return counts
}
fn ranked(counts: Map[string, int]): [](int, string) {
mut rows: [](int, string) = []
for word, n in counts {
rows.add((n, word))
}
return rows.sorted_descending()
}
fn top(words: []string): (string, int) {
rows := ranked(tally(words))
best := rows.get(0) ?? (0, "")
return (best.1, best.0)
}
fn report(words: []string): string {
word, count := top(words)
if count == 0 { return "no words" }
return "${word} appears ${count} time(s)"
}
fn f(): string {
return report(["a", "b", "a", "c", "a", "b"])
}ranked deliberately returns (count, word) rather than (word, count) so
that the default tuple ordering sorts by count first — a good example of when
positional order is doing real work, and of why top flips the pair back before
handing it to a caller who should not have to know.