Tuple
A tuple is a fixed-length, heterogeneous, immutable sequence of values. Literal and type share the same shape: (3, "a") has type (int, string). The component count and each component’s type are part of the type, so (int, string), (int, int), and (int, string, int) are all distinct types.
Literals and Types
func main() -> int
{
val t = (3, "a");
print(t, t is int);
val u: (int, string, float) = (1, "b", 2.5);
print(u.Item0, u.Item1, u.Item2);
val nest = ((1, 2), "x");
print(nest.Item0.Item1, nest);
val p = (1);
print(p, p is int);
return 0;
}(3, a) False
1 b 2.5
2 ((1, 2), x)
1 True- It takes at least two components to make a tuple:
(1)is a parenthesized expression and its type is stillint. - Nesting is legal: an outer component can itself be a tuple, drilled into level by level with
.Item. - Printing looks like
(3, a); string components carry no quotes.
Positional Members: Starting at Item0
Components are accessed through 0-based positional members; t.Item0 is the first component. Components are read-only; a tuple is an immutable value.
The component count lives in the type, so out-of-range access and wrong member names fail at compile time, never waiting for runtime:
func main() -> int
{
val t = (3, "a");
print(t.Item0, t.Item1);
return 0;
}3 aerror MS3281: Tuple '(int32, string)' has no component 'Item2': components are read-only .Item0 .. .Item1 (0-based).Indexing with t[0] is equivalent to .Item0; it accepts only in-range constant integers — dynamic expressions, out-of-range indices, and multi-argument indexing are rejected outright:
func main() -> int
{
val w: (int, string) = (3, "c");
print(w[0], w[1]);
return 0;
}3 cDestructuring and Multiple Return Values
The declaration position of val / var supports tuple destructuring, naming several components at once. Components destructured with var can be reassigned.
func DivMod(a: int, b: int) -> (int, int)
{
return (a / b, a % b);
}
func Sum(p: (int, int)) -> int
{
return p.Item0 + p.Item1;
}
func main() -> int
{
val (q, r) = DivMod(17, 5);
print(q, r);
print(Sum((3, 4)));
var (a, b) = (1, 2);
a = 9;
print(a, b);
return 0;
}3 2
7
9 2Returning a tuple from a function is the standard shape for multiple return values; the call site can destructure to catch them. Tuples can also be passed whole as arguments, and for a list whose element static type is a tuple, the for-in iteration variable position can destructure directly:
func main() -> int
{
val xs: list<(int, string)> = [(1, "a"), (2, "b")];
for (n, s) in xs
{
print(n, s);
}
return 0;
}1 a
2 blist.Zip and map.Entries() also produce lists of tuples; destructuring, annotated assignment, and .Item members all work (see Map).
Equality: Reference Identity
== / != compare instance identity, the same rule as class and containers: two tuples built separately are unequal even with identical contents; only aliases of the same instance are equal. To compare contents, destructure and compare component by component:
func main() -> int
{
val p = (1, "a");
val q = (1, "a");
val alias = p;
print(p == q, p == alias);
print(p.Item0 == q.Item0 && p.Item1 == q.Item1);
return 0;
}False True
TrueNullable tuples join in as usual: nullable against nullable, and mixed nullable / non-nullable pairs are all allowed; identity is compared after unwrapping, and == null checks work as always.
Division of Labor with Containers
Choose a tuple for data of fixed length whose structure is known at compile time, List for dynamic length and homogeneous elements, and let a heterogeneous collection degrade to list<any> plus runtime narrowing. The tuple’s advantage is moving structure up to compile time. Component types take part in function signatures and generic arguments (list<(int, string)>), and there is no add/remove mutation surface.
Notes
(1,)and()both fail to parse: a tuple needs at least two components, while a trailing comma with more than two components,(1, 2,), is still legal; empty parentheses are not an expression.- A nullable tuple itself null-check narrows as usual: inside an
if (pay != null)branch you can destructure directly or take a component withpay[0], but nullable components inside the tuple still do not narrow; destructure the component out first, then check it. - Tuple patterns in match and
isare covered in Patterns. A tuple value flowing through the any channel can be shape-tested by type shape: whenif e is (int, string) pmatches,pbinds the tuple value; a shape mismatch silently yields False. The untyped destructuring formis (k, v)is not supported.
