A tuple is a fixed number of values in a particular order, like (10, "foo"). The values
can have different types. Tuples are useful when you want to group a few values together, and
giving each one a name (like you would in a record) would be more trouble than it's
worth.
Tuple literals are written with parentheses and comma-separated values:
point = (10, 20)
mixed = ("hello", 42, True)
nested = ((1, 2), (3, 4))
Tuples have at least two elements. A single value in parentheses is just that value, not a tuple:
x = (42) # This is just 42, not a tuple
Tuple elements are accessed using dot notation with a zero-based numeric index:
point = (10, 20)
x = point.0 # 10
y = point.1 # 20
This works with any expression that evaluates to a tuple:
get_point = || (100, 200)
x = get_point().0 # 100
Chained access works for nested tuples:
nested = ((1, 2), (3, 4))
value = nested.0.1 # 2 (second element of the first tuple)
The index has to be a number written right there in the code, not a variable or anything
computed. That's because each element of a tuple can have a different type, so the compiler
needs to know which element you're accessing in order to know what type you get back. (It also
means there's no such thing as an out-of-bounds tuple access at runtime; using an index that's
too big is a compile-time error.)
Tuple types are written with parentheses containing comma-separated types:
point : (I64, I64)
point = (10, 20)
mixed : (Str, I64, Bool)
mixed = ("hello", 42, True)
Each position in a tuple can have a different type, and the type of each position is part of the tuple's type.
Tuples can be destructured in assignments and pattern matching:
point = (10, 20)
(x, y) = point # x = 10, y = 20
In match expressions:
describe_point = |point|
match point {
(0, 0) => "origin"
(0, _) => "on y-axis"
(_, 0) => "on x-axis"
(x, y) => "at (${x.to_str()}, ${y.to_str()})"
}
Tuples and records are very similar. Both group a fixed number of values together,
the values can have different types, and neither one involves a heap allocation. The difference is
that a record gives each of its values a name, whereas a tuple identifies its values only by position.
This makes records more self-documenting. Compare:
tuple_user = ("Sam", "sam@example.com", 30)
record_user = { name: "Sam", email: "sam@example.com", age: 30 }
With the tuple, you have to remember that tuple_user.1 is the email address, whereas with the
record, you can write record_user.email. Records also have features that tuples don't, like
optional fields and record update syntax.
Tuples are most useful when the meaning of each position is obvious from context, and the
group of values is small. Common examples include:
- Coordinates, like
(x, y) - Returning two values from a function, like a result and some updated state
- Key-value pairs, like the
(k, v) pairs used by Dict
When a tuple grows beyond two or three elements, or when it's not obvious what each position
means, a record is usually the better choice.
A tuple is stored exactly like a record whose fields are its
elements. It's stored inline, with no heap allocation and no reference count of its own, and
accessing an element is a read from a known offset.
As with records, the order of a tuple's elements in memory doesn't have to match the order
you wrote them in. The compiler sorts them by alignment (keeping elements with the same
alignment in their original order) to minimize padding. For example, on a 64-bit target,
(U8, U64, U8) takes 16 bytes, not the 24 it would take if its elements were stored in
order.