Tuple Types
A tuple is a fixed-size, ordered group of values that can each have a different type. The type of a tuple lists the type of every element in order: (i32, f64, &str) is a tuple of exactly three elements, an i32, an f64, and a &str. The number of elements and their types are part of the tuple's type and cannot change after it is created.
fn main() {
let person: (&str, u32, f64) = ("Ada", 36, 1.68);
println!("{}", person.0);
println!("{}", person.1);
println!("{}", person.2);
println!("{person:?}");
}
Ada
36
1.68
("Ada", 36, 1.68)
Tuples are compound types: they are built from other types. They differ from arrays in two ways: the elements may have different types, and elements are reached by a constant position, not by a runtime index.
Syntax
A tuple expression is a comma-separated list of expressions in parentheses. A tuple type is a comma-separated list of types in parentheses.
fn main() {
let a = (1, 2); // inferred: (i32, i32)
let b: (u8, bool) = (200, true); // annotated
let c = (1u64, "text", 'x', 2.5f32); // four elements, four types
let d = ((1, 2), (3, 4)); // tuples can be nested
println!("{a:?} {b:?} {c:?} {d:?}");
}
(1, 2) (200, true) (1, "text", 'x', 2.5) ((1, 2), (3, 4))
One-Element Tuples
A tuple with one element needs a trailing comma. Without the comma, the parentheses are only grouping, and the value is not a tuple.
fn main() {
let one = (5,);
println!("{one:?}");
}
(5,)
// ❌ does not compile
fn main() {
let n: (i32,) = (5);
println!("{n:?}");
}
error[E0308]: mismatched types
--> main.rs:3:21
|
3 | let n: (i32,) = (5);
| ------ ^^^ expected `(i32,)`, found integer
| |
| expected due to this
|
= note: expected tuple `(i32,)`
found type `{integer}`
help: use a trailing comma to create a tuple with one element
|
3 | let n: (i32,) = (5,);
| +
The Empty Tuple
() is the tuple with zero elements. It is a type with exactly one value, also written (), and it is called the unit type. Functions that return nothing return (). The unit type has its own section.
Accessing Elements
Index Access
An element is read with a dot, followed by its zero-based position as an integer literal: t.0, t.1, and so on. Nested tuples chain the accesses: nested.0.1.
fn main() {
let nested = ((1, 2), (3, 4));
println!("{}", nested.0.1);
println!("{}", (nested.1).0);
}
2
3
The position is part of the syntax, not a value. It must be a literal. A variable cannot be used, and a position past the end of the tuple is a compile error, not a runtime panic.
// ❌ does not compile
fn main() {
let t = (1, 2.5, "x");
println!("{}", t.3);
}
error[E0609]: no field `3` on type `({integer}, {float}, &str)`
--> main.rs:4:22
|
4 | println!("{}", t.3);
| ^ unknown field
|
= note: available fields are: `0`, `1`, `2`
// ❌ does not compile
fn main() {
let t = (1, 2, 3);
let i = 1;
println!("{}", t.i);
}
error[E0609]: no field `i` on type `({integer}, {integer}, {integer})`
--> main.rs:5:22
|
5 | println!("{}", t.i);
| ^ unknown field
|
help: a field with a similar name exists
|
5 - println!("{}", t.i);
5 + println!("{}", t.0);
|
This restriction exists because every element can have a different type. The type of t.i would depend on a runtime value, which a statically typed language cannot express. When elements are all the same type and need runtime indexing, use an array.
Mutation
Assigning to an element requires the whole tuple to be declared mut. Elements cannot be made mutable individually.
fn main() {
let mut point = (3, 4);
point.0 += 10;
point.1 = -1;
println!("{point:?}");
}
(13, -1)
// ❌ does not compile
fn main() {
let t = (1, 2);
t.0 = 5;
println!("{t:?}");
}
error[E0594]: cannot assign to `t.0`, as `t` is not declared as mutable
--> main.rs:4:5
|
4 | t.0 = 5;
| ^^^^^^^ cannot assign
|
help: consider changing this to be mutable
|
3 | let mut t = (1, 2);
| +++
An assignment can change an element's value but never its type, because the type is fixed.
Destructuring
Destructuring unpacks a tuple into separate variables with a pattern. The pattern has the same shape as the tuple.
fn main() {
let person = ("Ada", 36, 1.68);
let (name, age, height) = person;
println!("{name} is {age} years old and {height} m tall");
}
Ada is 36 years old and 1.68 m tall
The pattern must have the same number of elements as the tuple, otherwise the compiler reports the mismatch:
// ❌ does not compile
fn main() {
let (a, b) = (1, 2, 3);
println!("{a} {b}");
}
error[E0308]: mismatched types
--> main.rs:3:9
|
3 | let (a, b) = (1, 2, 3);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`
Ignoring Elements
_ ignores one element. .. ignores any number of elements and can appear once in a pattern, at the start, middle, or end.
fn main() {
let ((a, _), (_, d)) = ((1, 2), (3, 4));
println!("{a} {d}");
let (first, ..) = (1, 2, 3, 4);
let (.., last) = (1, 2, 3, 4);
let (x, .., z) = (1, 2, 3, 4);
println!("{first} {last} {x} {z}");
}
1 4
1 4 1 4
Destructuring Assignment
A pattern can also appear on the left of an assignment to existing variables. This swaps two values without a temporary:
fn main() {
let mut a = 1;
let mut b = 2;
(a, b) = (b, a);
println!("{a} {b}");
let mut pair = (0, 0);
(pair.0, pair.1) = (7, 8);
println!("{pair:?}");
}
2 1
(7, 8)
The right side is evaluated completely before any assignment happens, which is why the swap works.
Destructuring in Other Places
Tuple patterns work anywhere a pattern is allowed: let, match, if let, for, function parameters, and closure parameters.
fn distance_from_origin((x, y): (f64, f64)) -> f64 {
(x * x + y * y).sqrt()
}
fn divide(a: i32, b: i32) -> Option<(i32, i32)> {
if b == 0 { None } else { Some((a / b, a % b)) }
}
fn main() {
println!("{}", distance_from_origin((3.0, 4.0)));
if let Some((quotient, remainder)) = divide(17, 5) {
println!("{quotient} remainder {remainder}");
}
let add = |(a, b): (i32, i32)| a + b;
println!("{}", add((2, 3)));
for (index, ch) in "hey".chars().enumerate() {
println!("{index}:{ch}");
}
}
5
3 remainder 2
5
0:h
1:e
2:y
Matching on Tuples
match on a tuple tests several values at once. Each arm can combine literals, bindings, wildcards, and guards.
fn classify(pair: (i32, i32)) -> &'static str {
match pair {
(0, 0) => "origin",
(x, 0) if x > 0 => "positive x axis",
(_, 0) => "negative x axis",
(0, _) => "y axis",
_ => "somewhere else",
}
}
fn main() {
println!("{}", classify((0, 0)));
println!("{}", classify((4, 0)));
println!("{}", classify((-4, 0)));
println!("{}", classify((0, 9)));
println!("{}", classify((1, 1)));
}
origin
positive x axis
negative x axis
y axis
somewhere else
Returning Multiple Values
A function returns one value. A tuple lets that value carry several results, and the caller destructures them.
fn min_max(values: &[i32]) -> (i32, i32) {
let mut min = values[0];
let mut max = values[0];
for &v in values {
if v < min {
min = v;
}
if v > max {
max = v;
}
}
(min, max)
}
fn main() {
let (lo, hi) = min_max(&[4, -2, 9, 0]);
println!("{lo} {hi}");
}
-2 9
The standard library uses this pattern widely. Iterator::enumerate yields (index, item), zip yields pairs, partition returns two collections, and unzip splits a sequence of pairs.
fn main() {
let names = ["a", "b", "c"];
let scores = [10, 20, 30];
let zipped: Vec<(&str, i32)> = names.into_iter().zip(scores).collect();
println!("{zipped:?}");
let (n, s): (Vec<&str>, Vec<i32>) = zipped.into_iter().unzip();
println!("{n:?} {s:?}");
let (evens, odds): (Vec<i32>, Vec<i32>) = (1..=6).partition(|n| n % 2 == 0);
println!("{evens:?} {odds:?}");
}
[("a", 10), ("b", 20), ("c", 30)]
["a", "b", "c"] [10, 20, 30]
[2, 4, 6] [1, 3, 5]
Tuple elements have no names. For results with more than two or three elements, or where the meaning of each position is not obvious, a struct with named fields is clearer.
Ownership: Copy and Move
A tuple owns its elements. It implements Copy when every element is Copy. Otherwise it is moved like any non-Copy value.
fn main() {
let a = (1, 2.5, 'c');
let b = a;
println!("{a:?} {b:?}");
let owned = (String::from("hi"), 5);
let moved = owned;
println!("{moved:?}");
}
(1, 2.5, 'c') (1, 2.5, 'c')
("hi", 5)
a is still usable after let b = a because (i32, f64, char) is Copy. owned would not be usable after the move because String is not Copy.
Partial Moves
Moving one non-Copy element out of a tuple leaves the other elements usable, but the tuple as a whole can no longer be used.
fn main() {
let partial = (String::from("left"), String::from("right"));
let left = partial.0;
println!("{left} {}", partial.1);
}
left right
// ❌ does not compile
fn main() {
let owned = (String::from("a"), String::from("b"));
let first = owned.0;
println!("{owned:?}");
println!("{first}");
}
error[E0382]: borrow of partially moved value: `owned`
--> main.rs:5:16
|
4 | let first = owned.0;
| ------- value partially moved here
5 | println!("{owned:?}");
| ^^^^^ value borrowed here after partial move
|
= note: partial move occurs because `owned.0` has type `String`, which does not implement the `Copy` trait
Borrowing Elements
A reference to a tuple can be destructured directly. The bindings become references to the elements, so &mut gives mutable access to each one.
fn main() {
let mut pair = (1, 2);
let (x, y) = &mut pair;
*x += 10;
*y += 20;
println!("{pair:?}");
let shared = &pair;
println!("{}", shared.0);
}
(11, 22)
11
Field access through a reference (shared.0) needs no explicit dereference.
Traits Implemented by Tuples
The standard library implements these traits for tuples whose elements all implement them:
| Trait | Behavior for a tuple |
|---|---|
Debug |
Prints as (a, b, c); {:#?} prints one element per line |
Clone, Copy |
Clones or copies every element |
PartialEq, Eq |
Equal when every element is equal |
PartialOrd, Ord |
Lexicographic: compares the first elements, then the second if they are equal, and so on |
Hash |
Hashes every element |
Default |
A tuple of each element's default value |
Display is not implemented for tuples. Printing a tuple with {} is an error; use {:?}.
// ❌ does not compile
fn main() {
let t = (1, 2.5);
println!("{t}");
}
error[E0277]: `({integer}, {float})` doesn't implement `std::fmt::Display`
--> main.rs:4:15
|
4 | println!("{t}");
| ^^^ `({integer}, {float})` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `({integer}, {float})`
= note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead
Pretty-printing with {:#?}:
fn main() {
let person = ("Ada", 36, 1.68);
println!("{person:#?}");
}
(
"Ada",
36,
1.68,
)
Comparison
Ordering is lexicographic, so a later element only matters when all earlier elements are equal. Both tuples must have the same type; tuples of different shapes cannot be compared.
fn main() {
println!("{}", (1, "z") < (2, "a"));
println!("{}", (1, 5) < (1, 6));
println!("{}", (1, 2, 3) == (1, 2, 3));
}
true
true
true
// ❌ does not compile
fn main() {
let a = (1, "x");
let b = (1, 2.0);
println!("{}", a == b);
}
error[E0308]: mismatched types
--> main.rs:5:25
|
5 | println!("{}", a == b);
| ^ expected `({integer}, &str)`, found `({integer}, {float})`
|
= note: expected tuple `({integer}, &str)`
found tuple `({integer}, {float})`
Lexicographic ordering makes tuples convenient as multi-field sort keys and as composite HashMap keys:
use std::collections::HashMap;
fn main() {
let mut people = vec![("Bob", 25), ("Alice", 30), ("Carol", 25)];
people.sort_by_key(|&(name, age)| (age, name));
println!("{people:?}");
let mut grid: HashMap<(i32, i32), &str> = HashMap::new();
grid.insert((0, 0), "start");
grid.insert((2, 3), "goal");
println!("{:?}", grid.get(&(2, 3)));
println!("{:?}", grid.get(&(9, 9)));
let d: (i32, bool, String) = Default::default();
println!("{d:?}");
}
[("Bob", 25), ("Carol", 25), ("Alice", 30)]
Some("goal")
None
(0, false, "")
The Arity Limit
These trait implementations exist for tuples of up to 12 elements. A 13-element tuple is a valid type, but it does not implement Debug, PartialEq, or the other traits.
// ❌ does not compile
fn main() {
let thirteen = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
println!("{thirteen:?}");
}
error[E0277]: `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})` doesn't implement `Debug`
--> main.rs:4:15
|
4 | println!("{thirteen:?}");
| ^^^^^^^^^^^^ `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})` cannot be formatted using `{:?}` because it doesn't implement `Debug`
|
= help: the trait `Debug` is not implemented for `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})`
= help: the following other types implement trait `Debug`:
(T₁, T₂, …, Tₙ) up to tuples of arity 12
and 5 others
A tuple that large should be a struct.
Tuples Are Not Iterable
A tuple cannot be used in a for loop. Its elements have different types, so there is no single item type to iterate over.
// ❌ does not compile
fn main() {
let t = (1, 2, 3);
for x in t {
println!("{x}");
}
}
error[E0277]: `({integer}, {integer}, {integer})` is not an iterator
--> main.rs:4:14
|
4 | for x in t {
| ^ `({integer}, {integer}, {integer})` is not an iterator
|
= help: the trait `Iterator` is not implemented for `({integer}, {integer}, {integer})`
= note: required for `({integer}, {integer}, {integer})` to implement `IntoIterator`
Use an array when the elements share a type and need to be looped over.
Memory Layout
The elements of a tuple are stored inline, one after another, with no heap allocation and no header. A (i32, f64) occupies the space of one i32, any padding for alignment, and one f64.
For tuples, the compiler is free to reorder elements to reduce padding. The layout of a tuple is not specified, so code must not depend on element order in memory. The sizes below were printed on a 64-bit machine:
use std::mem::size_of;
fn main() {
println!("{}", size_of::<()>());
println!("{}", size_of::<(u8,)>());
println!("{}", size_of::<(u8, u32)>());
println!("{}", size_of::<(u8, u32, u8)>());
println!("{}", size_of::<(u8, u8, u32)>());
println!("{}", size_of::<(u64, u8)>());
}
0
1
8
8
8
16
(u8, u32, u8) has the same size as (u8, u8, u32): the compiler places the two u8 values next to each other and the u32 after them. Without reordering, the first form would need padding after each u8 and take 12 bytes.
(u8, u32, u8) as declared: 1 + 3 pad + 4 + 1 + 3 pad = 12 bytes
(u8, u32, u8) as laid out: u32 | u8 | u8 | 2 pad = 8 bytes
Tuples, Arrays, and Tuple Structs
| Tuple | Array | Tuple struct | |
|---|---|---|---|
| Element types | May differ | All the same | May differ |
| Length | Fixed, part of the type | Fixed, part of the type | Fixed |
| Element access | t.0 (literal position) |
a[i] (runtime index) |
s.0 (literal position) |
| Iterable | No | Yes | No |
| Named type | No, structural | No, structural | Yes, struct Meters(f64); |
| Own trait implementations | Only the std ones | Only the std ones | Any, including your own |
Two tuples have the same type when their element types match, regardless of purpose. A tuple struct is a distinct type, so struct Meters(f64) and struct Feet(f64) cannot be mixed up even though both wrap an f64.