Tuples are fixed-size ordered values that can hold mixed types. They come in two forms: positional and named.

#Positional Tuples

Use Tuple(...) to create a tuple with values accessed by numeric index:

let t = Tuple("hello", 42);
Println(t.0);       // "hello"
Println(t.1);       // "42"

With a type annotation:

let pair: Tuple(Str,Int) = Tuple("age", 30);

#Named Tuples

Named tuples add field names so you can access elements by name. Use = for values and : for types:

let person = Tuple(name="Alice", age=30);
Println(person.name);       // "Alice"
Println(person.age);        // "30"

Numeric index access still works on named tuples:

Println(person.0);          // "Alice"
Println(person.1);          // "30"

With a type annotation:

let t: Tuple(key: Str, value: Int) = Tuple(key="x", value=42);

A tuple must be all-named or all-positional. Mixing is a compile error:

let bad = Tuple(name="foo", 42);  // error: cannot mix named and positional

Named and positional tuples are distinct types — you cannot assign one to the other:

let t: Tuple(name: Str, age: Int) = Tuple("Alice", 30);  // error: type mismatch

#Function Parameters and Return Types

Tuples work in function signatures like any other type. For multi-return functions, use the (T1, T2) shorthand return type:

fn min_max(nums: Array(Int)) -> (Int,Int) {
    var lo = nums[0];
    var hi = nums[0];
    for n in nums {
        if n < lo { lo = n; }
        if n > hi { hi = n; }
    }
    return lo, hi;
}

fn main() {
    let (lo, hi) = min_max([3, 1, 4, 1, 5, 9]);
    Println("{lo} {hi}");  // "1 9"
}

The (T1, T2) return type is shorthand for Tuple(T1, T2). The return a, b; syntax is shorthand for return Tuple(a, b);.

Named tuples also work in signatures:

fn make_person(name: Str, age: Int) -> Tuple(name: Str, age: Int) {
    return Tuple(name=name, age=age);
}

fn greet(person: Tuple(name: Str, age: Int)) {
    Println(person.name + " is " + Str(person.age));
}

fn main() {
    let p = make_person("Alice", 30);
    greet(p);
}

#Destructuring

Tuple destructuring binds each element to a variable by position:

let t = Tuple("hello", 42);
let (greeting, number) = t;
Println(greeting);       // "hello"
Println(number);         // "42"

This works with named tuples too — the names don’t affect destructuring order:

let person = Tuple(name="Alice", age=30);
let (n, a) = person;
Println(n);              // "Alice"

#Destructuring in For-In Loops

Tuple destructuring also works in for-in loop headers. When iterating over an array of tuples, use (a, b) to bind each element directly:

let names = ["Alice", "Bob"];
let scores = [95, 87];

for (name, score) in Zip(names, scores) {
    Println("{name}: {score}");
}
// Alice: 95
// Bob: 87

This works with .enumerate() and any array of tuples:

let items = ["apple", "banana", "cherry"];
for (i, item) in items.enumerate() {
    Println("{i}: {item}");
}

let pairs = [Tuple(1, "x"), Tuple(2, "y")];
for (num, letter) in pairs {
    Println("{num}={letter}");
}

Use _ to discard positions you don’t need:

for (_, val) in pairs {
    Println(val);
}

#Tuples as Hashmap and Set Keys

Tuples with comparable element types (built-in types like Int, Str, Float, Bool) can be used as hashmap keys and set elements:

var grid = Hashmap(Tuple(Int,Int),Str);
grid[Tuple(0, 0)] = "origin";
grid[Tuple(1, 2)] = "point";
Println(grid[Tuple(0, 0)]);  // "origin"

var visited = Set(Tuple(Int,Int));
visited.add(Tuple(3, 4));
Println(visited.exists(Tuple(3, 4)));  // true

See the Hashmaps and Sets docs for more details.

#Arrays of Tuples

Tuples nest naturally inside arrays:

let pairs: Array(Tuple(Str,Str)) = [];
pairs.add(Tuple("left", "right"));
Println(pairs[0].0 + ":" + pairs[0].1);

With named tuples:

let people: Array(Tuple(name: Str, score: Int)) = [];
people.add(Tuple(name="Bob", score=95));
people.add(Tuple(name="Carol", score=88));

for p in people {
    Println(p.name + ": " + Str(p.score));
}