Compound Types
How Rust groups multiple values into a single structure using tuples and arrays, the two primitive compound data types
Rust's scalar types—integers, floats, booleans, and characters—each hold exactly one value at a time. Real programs need to work with collections of related data: coordinates, RGB colors, lists of sensor readings, rows from a database. Compound types fill that gap by bundling multiple values into a single variable.
Rust provides two primitive compound types: tuples and arrays. Both have a fixed size known at compile time, which sets them apart from growable collections like vectors or strings. The distinction matters for memory layout, performance, and how you think about ownership.
Tuples
A tuple groups a fixed number of values, each of which can have a different type. They are the go-to choice when you need to return more than one value from a function, or when a small ad-hoc grouping is more readable than defining a full struct.
Creating a Tuple and Type Annotations
A tuple literal uses parentheses and commas. Each position in the tuple holds a separate value, and the type of the tuple reflects the type of each position.
let location: (&str, f64, f64) = ("Nairobi", -1.286389, 36.817223);
The type annotation (&str, f64, f64) reads as "a tuple containing a string slice, a 64-bit float, and another 64-bit float." The compiler infers this type if you omit the annotation, but writing it out makes the intent explicit in function signatures or complex expressions.
The number of elements—the arity—is part of the type. A 2-element tuple and a 3-element tuple are completely different types, even if the individual element types overlap. This is why a function that returns (i32, i32) cannot be used where a (i32, i32, i32) is expected.
Accessing Elements by Index
You can pull individual values out of a tuple using dot notation followed by the zero-based index. The index must be a compile-time constant; there is no runtime variable-based access.
let location = ("Nairobi", -1.286389, 36.817223);
let city = location.0;
let lat = location.1;
let lng = location.2;
This works well when the tuple's meaning is obvious from context—like coordinates—but becomes cryptic with larger tuples. There is no .first() or .last() method; the dot-index is the only direct access mechanism besides destructuring.
Tuple Indexes Are Not Iterable:
Unlike arrays, you cannot loop over a tuple with a for loop. Each position has a potentially different type, so there is no uniform way to treat every element. If you catch yourself wanting to iterate, a tuple is likely the wrong data structure.
Destructuring Tuples
Pattern matching on the left side of a let statement unpacks a tuple into separate variables in one move. This often replaces multiple index accesses and makes the code self-documenting.
let article = ("geeksforgeeks", "kushwanthreddy", 14, 12, 2020);
let (site, author, day, month, year) = article;
println!("Written by {author} on {site} at {day}/{month}/{year}");
Destructuring also handles partial extraction with .. to ignore fields you do not need:
let (_, lat, lng) = location;
println!("Coordinates: {lat}, {lng}");
What makes destructuring particularly useful is that it works everywhere a pattern is allowed: let bindings, function arguments, match arms, and if let expressions. A function that receives a tuple can decompose it directly in its parameter list:
fn distance((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 {
((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
}
Tuples and Functions — Multiple Return Values
Rust functions can return exactly one value. A tuple is one value that happens to contain several others, making it the standard way to return multiple results.
fn min_max(values: &[i32]) -> (i32, i32) {
let min = values.iter().min().copied().unwrap_or(0);
let max = values.iter().max().copied().unwrap_or(0);
(min, max)
}
let (low, high) = min_max(&[4, 1, 9, 3]);
The type (i32, i32) in the return position signals the caller exactly what to expect. Destructuring at the call site, as above, gives both results named variables immediately.
Comparing and Assigning Whole Tuples
Tuples support equality comparison with == and != as long as every element type implements the PartialEq trait—which all primitive types do. Order matters: (1, 2) == (2, 1) evaluates to false.
let a = (1, "hello");
let b = (1, "hello");
assert!(a == b);
Assignment between tuples works when both sides have the same type. The entire tuple is moved or copied at once, depending on the element types.
let mut x = (1, 2);
let y = (2, 3);
x = y; // x is now (2, 3)
Tuples Are Fixed-Size:
There is no push, pop, or insert on a tuple. Once created, its length and element types cannot change. If you need a collection that grows, use a Vec.
Arrays
An array is a fixed-length sequence of elements that all share the same type. Arrays are allocated entirely on the stack, making access extremely fast, but their size must be known at compile time.
Declaring an Array
An array literal uses square brackets with a comma-separated list of values. The type annotation includes both the element type and the length, separated by a semicolon.
let primes: [u32; 5] = [2, 3, 5, 7, 11];
For repeated initial values, Rust provides a shorthand:
let zeros = [0; 100]; // 100 zeros
let buffer = [0u8; 1024]; // 1024 bytes of zero
The length is a compile-time constant expression. The type [u8; 1024] is a distinct type from [u8; 512]; you cannot pass one where the other is expected.
Accessing Elements and Bounds Checking
Array indexing uses square brackets with a zero-based position. Rust checks every index at runtime against the array's length. An out-of-bounds access causes an immediate panic—an intentional crash—rather than reading arbitrary memory.
let primes = [2, 3, 5, 7, 11];
let third = primes[2]; // 5
Out-of-Bounds Indexes Cause a Panic:
Accessing primes[10] does not return garbage or wrap around; the program stops with a panic. This is a safety guarantee: Rust refuses to proceed with corrupted state. Languages like C allow out-of-bounds access that silently reads or writes adjacent memory, which is a major source of security vulnerabilities. Rust eliminates that entire class of bugs by design.
If you need to handle the possibility of an invalid index gracefully, use the .get() method, which returns an Option<&T>:
match primes.get(10) {
Some(value) => println!("Value: {value}"),
None => println!("Index out of range"),
}
This pattern is common when dealing with user input or data from external sources where the index might not be trustworthy.
Iterating Over an Array
Because all elements share a type, arrays are iterable. A for loop yields each element by value (which copies or moves, depending on the type) or by reference.
let values = [10, 20, 30, 40];
for v in &values {
println!("{v}");
}
The &values borrows the array, so the original remains usable afterward. Iterating by reference is the idiomatic default unless you specifically need ownership of the elements.
Arrays vs. Vectors — Choosing the Right Tool
Arrays and vectors serve overlapping purposes, but the fixed-size constraint of arrays is both a limitation and a strength. Use an array when the number of elements is genuinely fixed and known at compile time: days in a week, directions on a compass, the header fields of a fixed-format message. Use a vector (Vec<T>) when the collection needs to grow, shrink, or be built from runtime data.
Stack allocation makes arrays fast and predictable, but large arrays can overflow the stack. The standard library caps the default stack size, so an array of millions of elements will likely cause a stack overflow at runtime. Vectors allocate on the heap and avoid this problem. As a rule of thumb, arrays up to a few thousand elements are fine; beyond that, a vector is safer.
When to Choose Each:
Arrays are correct when the size is a constant known at compile time and the data fits comfortably on the stack. Vectors are correct when the size varies at runtime or when the amount of data is large. Recognizing this distinction early prevents refactoring later.
Tuples and Arrays Side by Side
Both compound types group multiple values, but they solve different organizational problems. Tuples handle heterogeneous data—different types in a known order. Arrays handle homogeneous data—many items of the same type, accessed by position.
| Feature | Tuple | Array |
|---|---|---|
| Element types | Can differ | Must be identical |
| Length | Fixed, part of type | Fixed, part of type |
| Access | .0, .1, destructuring | [index], .get(index) |
| Iteration | Not possible | for loop over elements |
| Memory location | Stack (inline) | Stack (entire array) |
| Typical use | Multiple return values, ad-hoc grouping | Fixed-size lists, buffers |
Neither Grows Dynamically:
Neither tuples nor arrays can change size after creation. For growable collections, Rust provides Vec<T> for vectors and String for text, covered in later sections.
Summary
Tuples and arrays are Rust's built-in tools for grouping values while retaining fixed sizes and stack allocation. They enforce compile-time knowledge of every element count and type, which eliminates entire categories of runtime errors that plague dynamically sized collections in other languages.
The key takeaway is that the choice between them is driven by whether the elements have a uniform type. If every element is the same kind of data, an array is natural; if the elements differ in type or carry distinct meanings, a tuple makes the structure self-documenting.
Tuples and arrays form the basis for more advanced data structures.