4.11Tuples and the unit type
Chapter 2 left a question on the table: a function returns exactly one value, so what do you do when the answer is two things? Division, for instance, produces a quotient and a remainder, and computing them in separate functions would do the same work twice. The missing piece is a way to make two values travel as one.
Tuples
A tuple groups a fixed number of values, possibly of different types, into a single compound value:
fn main() {
let player = ("Ada", 250, true);
println!("{}", player.0);
println!("{}", player.1);
println!("{}", player.2);
}Ada
250
true
Parentheses and commas build it; .0, .1, .2 access the pieces by position (numbering from zero, the convention nearly everything in programming uses). This tuple's type is written the way it looks: (&str, i32, bool). The length and the per-position types are fixed at compile time, like everything else in this chapter; a (i32, i32) and a (i32, i32, i32) are different, unmixable types.
The .0 style works, and there's usually something better: destructuring, which unpacks a tuple into named variables in one let:
fn main() {
let point = (3, 7);
let (x, y) = point;
println!("x={x} y={y}");
}x=3 y=7
The pattern on the left mirrors the shape on the right, and each name binds to its position. Don't need one of the pieces? The underscore from lesson 1.4 generalizes: let (x, _) = point; keeps the first and deliberately discards the second, warning-free.
Multiple return values
Now pay off the chapter 2 question. A function returns one value; a tuple is one value; therefore:
fn div_rem(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
fn main() {
let (quotient, remainder) = div_rem(17, 5);
println!("17 / 5 is {quotient} remainder {remainder}");
}17 / 5 is 3 remainder 2
Build the tuple in the tail expression, destructure it at the call site, and both answers arrive in one trip with honest names on arrival. This pattern is everywhere in real Rust, and it quietly retires a whole C++ idiom (out-parameters: passing a variable in so the function can scribble the second answer into it) that you now never need to learn.
Best practice
Tuples shine at small and short-lived: a pair crossing a function boundary, a quick regrouping inside a calculation. By three-ish elements, position stops being meaning (.2... the score? the level?), and the tool built for named fields is chapter 10's struct. If you're tempted to write a comment explaining what .1 is, you're being tempted toward a struct.
(A printing note before the quiz asks: println!("{}", point) doesn't work on a whole tuple; print the pieces, or wait one chapter; lesson 5.5's {:?} exists for exactly this.)
The unit type, unmasked
One more reveal and the chapter's type roster is complete. Write a tuple with zero elements: (). Fixed length (zero), known types (none), one possible value (the empty grouping, also written ()). Now compare with lesson 1.11's "unit type, written (), with exactly one value, also written ()."
Same thing. The unit type is the empty tuple. It was never a special case bolted onto the language: "no useful value" is just the zero-element grouping, the empty box, and everything you learned about it (statements produce it, no-arrow functions return it, semicolons discard into it) is ordinary tuple behavior at length zero. Rust has fewer one-off rules than its reputation suggests; it has a few ideas applied all the way down, and you've just watched two lessons' worth of machinery turn out to be one.
Quiz time
Question #1
What does this print?
fn main() {
let pair = (10, 4);
let (big, small) = pair;
println!("{}", big - small);
println!("{}", pair.1 - pair.0);
}Show solution
6
-6
Destructuring names the positions (10 and 4), so big - small is 6; the second line indexes the same tuple directly in the other order. (And note the names big/small are our claim, not checked truth: destructuring binds by position, and nothing stops a (4, 10) from arriving. Naming honestly is still your job.)
Question #2
Write min_max(a: i32, b: i32) -> (i32, i32) returning the smaller value first, and a main that destructures and prints both. (Lesson 4.7 gave you exactly the tool for the body.)
Show solution
fn min_max(a: i32, b: i32) -> (i32, i32) {
if a < b { (a, b) } else { (b, a) }
}
fn main() {
let (low, high) = min_max(9, 3);
println!("low {low}, high {high}");
}low 3, high 9
The if expression's arms each produce a tuple, the function's tail hands one back, and the call site unpacks it. Three chapters of machinery in four lines, all load-bearing.
Question #3
Predict the compiler's reaction:
fn main() {
let pair = (1, 2);
println!("{}", pair.2);
}Show solution
A compile error: a (i32, i32) has fields .0 and .1, and .2 doesn't exist (the error names the tuple type and the unknown field). Tuple length is part of the type, so "index out of bounds" on a tuple is impossible at runtime; there's no runtime left for it to happen in.
That's every fundamental type, plus decisions, plus grouping. The chapter summary and its quiz are next, and the quiz finally has enough ingredients to ask for real programs.