Rust - Tuples
Overview
Estimated time: 25–35 minutes
Learn how to use tuples in Rust for grouping multiple values of different types. Master tuple creation, destructuring, indexing, and understand when tuples are the right choice for your data structures.
Learning Objectives
- Create and initialize tuples with different types.
- Access tuple elements using indexing and destructuring.
- Use tuples for multiple return values from functions.
- Understand the unit type
()
and its significance. - Apply tuple patterns in match expressions.
Prerequisites
Creating Tuples
Basic Tuple Creation
Tuples group multiple values of potentially different types:
fn main() {
// Creating tuples with different types
let person = ("Alice", 25, true);
let coordinates = (10.5, -3.2);
let mixed = (42, "Hello", 3.14, false);
// Explicit type annotations (optional)
let point: (i32, i32) = (100, 200);
let info: (&str, u32, bool) = ("Bob", 30, false);
// Empty tuple (unit type)
let unit = ();
println!("Person: {:?}", person); // ("Alice", 25, true)
println!("Coordinates: {:?}", coordinates); // (10.5, -3.2)
println!("Mixed: {:?}", mixed); // (42, "Hello", 3.14, false)
println!("Point: {:?}", point); // (100, 200)
println!("Unit: {:?}", unit); // ()
}
Tuple Type Signatures
Understanding tuple types and their syntax:
fn main() {
// Different tuple types
let two_ints: (i32, i32) = (1, 2);
let three_mixed: (i32, f64, char) = (42, 3.14, 'x');
let nested: ((i32, i32), (f64, f64)) = ((1, 2), (3.14, 2.71));
// Single element tuple (note the comma!)
let single_element: (i32,) = (42,);
// Without comma, it's just a value in parentheses
let not_a_tuple = (42); // This is just an i32
println!("Two ints: {:?}", two_ints);
println!("Three mixed: {:?}", three_mixed);
println!("Nested: {:?}", nested);
println!("Single element: {:?}", single_element);
println!("Not a tuple: {:?}", not_a_tuple);
}
Accessing Tuple Elements
Index-Based Access
Access tuple elements using dot notation with numeric indices:
fn main() {
let student = ("Emma", 20, "Computer Science", 3.8);
// Access elements by index (zero-based)
let name = student.0;
let age = student.1;
let major = student.2;
let gpa = student.3;
println!("Name: {}", name); // Emma
println!("Age: {}", age); // 20
println!("Major: {}", major); // Computer Science
println!("GPA: {}", gpa); // 3.8
// Direct usage in expressions
println!("Student {} is {} years old", student.0, student.1);
// Modifying mutable tuples
let mut point = (0, 0);
point.0 = 10;
point.1 = 20;
println!("Updated point: {:?}", point); // (10, 20)
}
Destructuring Tuples
Extract tuple values into separate variables:
fn main() {
let rgb = (255, 128, 0);
// Basic destructuring
let (red, green, blue) = rgb;
println!("Red: {}, Green: {}, Blue: {}", red, green, blue);
// Partial destructuring with underscore
let person = ("John", 35, "Engineer", 75000);
let (name, age, _, salary) = person;
println!("{} is {} years old and earns ${}", name, age, salary);
// Nested tuple destructuring
let nested = ((1, 2), (3, 4));
let ((a, b), (c, d)) = nested;
println!("Values: {}, {}, {}, {}", a, b, c, d);
// Using .. to ignore multiple elements
let data = (1, 2, 3, 4, 5);
let (first, .., last) = data;
println!("First: {}, Last: {}", first, last); // First: 1, Last: 5
}
Tuples in Functions
Return Multiple Values
Use tuples to return multiple values from functions:
fn calculate_circle_properties(radius: f64) -> (f64, f64) {
let area = std::f64::consts::PI * radius * radius;
let circumference = 2.0 * std::f64::consts::PI * radius;
(area, circumference)
}
fn divide_with_remainder(dividend: i32, divisor: i32) -> Option<(i32, i32)> {
if divisor == 0 {
None
} else {
Some((dividend / divisor, dividend % divisor))
}
}
fn parse_coordinate(input: &str) -> Result<(i32, i32), String> {
let parts: Vec<&str> = input.split(',').collect();
if parts.len() != 2 {
return Err("Invalid format".to_string());
}
let x = parts[0].trim().parse::()
.map_err(|_| "Invalid x coordinate".to_string())?;
let y = parts[1].trim().parse::()
.map_err(|_| "Invalid y coordinate".to_string())?;
Ok((x, y))
}
fn main() {
// Using returned tuples
let (area, circumference) = calculate_circle_properties(5.0);
println!("Circle area: {:.2}, circumference: {:.2}", area, circumference);
// Handling optional tuples
match divide_with_remainder(17, 5) {
Some((quotient, remainder)) => {
println!("17 ÷ 5 = {} remainder {}", quotient, remainder);
}
None => println!("Cannot divide by zero"),
}
// Handling result tuples
match parse_coordinate("10, 20") {
Ok((x, y)) => println!("Parsed coordinates: ({}, {})", x, y),
Err(e) => println!("Parse error: {}", e),
}
}
Function Parameters
Accept tuples as function parameters:
fn distance_from_origin(point: (f64, f64)) -> f64 {
let (x, y) = point;
(x * x + y * y).sqrt()
}
fn print_person_info((name, age, city): (&str, u32, &str)) {
println!("{} is {} years old and lives in {}", name, age, city);
}
fn swap_coordinates((x, y): (i32, i32)) -> (i32, i32) {
(y, x)
}
fn main() {
let point = (3.0, 4.0);
println!("Distance from origin: {}", distance_from_origin(point));
let person = ("Alice", 28, "New York");
print_person_info(person);
let coords = (10, 20);
let swapped = swap_coordinates(coords);
println!("Original: {:?}, Swapped: {:?}", coords, swapped);
}
Pattern Matching with Tuples
Match Expressions
Use tuples in pattern matching:
fn classify_point(point: (i32, i32)) -> &'static str {
match point {
(0, 0) => "origin",
(0, _) => "on y-axis",
(_, 0) => "on x-axis",
(x, y) if x == y => "on diagonal",
(x, y) if x > 0 && y > 0 => "first quadrant",
(x, y) if x < 0 && y > 0 => "second quadrant",
(x, y) if x < 0 && y < 0 => "third quadrant",
(x, y) if x > 0 && y < 0 => "fourth quadrant",
_ => "unknown",
}
}
fn process_rgb(color: (u8, u8, u8)) -> String {
match color {
(255, 0, 0) => "pure red".to_string(),
(0, 255, 0) => "pure green".to_string(),
(0, 0, 255) => "pure blue".to_string(),
(r, g, b) if r == g && g == b => format!("grayscale: {}", r),
(r, g, b) if r > 200 && g > 200 && b > 200 => "light color".to_string(),
(r, g, b) if r < 50 && g < 50 && b < 50 => "dark color".to_string(),
(r, g, b) => format!("RGB({}, {}, {})", r, g, b),
}
}
fn main() {
let points = [(0, 0), (0, 5), (3, 0), (3, 3), (2, -1), (-1, 2)];
for point in points.iter() {
println!("{:?} is {}", point, classify_point(*point));
}
let colors = [(255, 0, 0), (128, 128, 128), (10, 10, 10), (100, 150, 200)];
for color in colors.iter() {
println!("{:?} -> {}", color, process_rgb(*color));
}
}
The Unit Type
Understanding ()
The unit type represents no meaningful value:
fn print_message(msg: &str) -> () {
println!("{}", msg);
// Implicit return of ()
}
fn do_something() {
// Functions that don't return a value return ()
let x = 5;
let y = 10;
// No explicit return
}
fn main() {
// Functions that don't return values return the unit type
let result1 = print_message("Hello");
let result2 = do_something();
println!("Result 1: {:?}", result1); // ()
println!("Result 2: {:?}", result2); // ()
// Unit type in match expressions
let value = 42;
match value {
42 => println!("Found the answer!"), // Returns ()
_ => println!("Not the answer"), // Returns ()
}
// Unit type with semicolon
let _unit1 = (); // Explicit unit value
let _unit2 = {}; // Block that evaluates to ()
}
Advanced Tuple Usage
Tuple Collections
Working with collections of tuples:
fn main() {
// Vector of tuples
let students = vec![
("Alice", 85),
("Bob", 92),
("Charlie", 78),
("Diana", 94),
];
// Process tuple collections
let total_score: i32 = students.iter()
.map(|(_, score)| score)
.sum();
let average = total_score as f64 / students.len() as f64;
println!("Average score: {:.1}", average);
// Find best student
let best_student = students.iter()
.max_by_key(|(_, score)| score);
if let Some((name, score)) = best_student {
println!("Best student: {} with score {}", name, score);
}
// Group by score range
let mut excellent = Vec::new();
let mut good = Vec::new();
let mut needs_improvement = Vec::new();
for (name, score) in students {
match score {
90..=100 => excellent.push((name, score)),
80..=89 => good.push((name, score)),
_ => needs_improvement.push((name, score)),
}
}
println!("Excellent: {:?}", excellent);
println!("Good: {:?}", good);
println!("Needs improvement: {:?}", needs_improvement);
}
Tuple Iteration
Iterate over tuples and tuple collections:
fn main() {
let coordinates = vec![(0, 0), (1, 2), (3, 4), (5, 6)];
// Iterate with destructuring
for (x, y) in coordinates.iter() {
println!("Point: ({}, {}), Distance: {:.2}",
x, y, (*x as f64).hypot(*y as f64));
}
// Enumerate with tuples
let names = vec!["Alice", "Bob", "Charlie"];
for (index, name) in names.iter().enumerate() {
println!("{}: {}", index + 1, name);
}
// Zip to create tuples
let first_names = vec!["John", "Jane", "Bob"];
let last_names = vec!["Doe", "Smith", "Johnson"];
let ages = vec![30, 25, 35];
let people: Vec<_> = first_names.iter()
.zip(last_names.iter())
.zip(ages.iter())
.map(|((first, last), age)| (*first, *last, *age))
.collect();
for (first, last, age) in people {
println!("{} {} is {} years old", first, last, age);
}
}
When to Use Tuples
Appropriate Use Cases
- Temporary grouping: When you need to group a few values briefly
- Function returns: Returning multiple related values
- Coordinates: Simple x, y pairs or RGB values
- Key-value pairs: When you don't need the overhead of HashMap
When to Avoid Tuples
- Many elements: Use structs for more than 3-4 elements
- Named fields needed: Use structs when field names add clarity
- Complex behavior: Use structs when you need methods
- Long-term storage: Structs are more maintainable for persistent data
Common Pitfalls
Mistakes to Avoid
- Single-element tuples: Remember the comma:
(value,)
- Index confusion: Tuple indices start at 0, not 1
- Type mismatches: Tuple types must match exactly
- Overusing tuples: Use structs for better code readability
Checks for Understanding
- How do you create a single-element tuple?
- What's the difference between
(42)
and(42,)
? - How do you ignore the middle elements when destructuring a 5-element tuple?
- What type does a function return if it doesn't explicitly return anything?
Answers
- Use a trailing comma:
let single = (42,);
(42)
is just an integer in parentheses;(42,)
is a single-element tuple- Use
..
:let (first, .., last) = five_tuple;
- It returns the unit type
()