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

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

When to Avoid Tuples

Common Pitfalls

Mistakes to Avoid

Checks for Understanding

  1. How do you create a single-element tuple?
  2. What's the difference between (42) and (42,)?
  3. How do you ignore the middle elements when destructuring a 5-element tuple?
  4. What type does a function return if it doesn't explicitly return anything?
Answers
  1. Use a trailing comma: let single = (42,);
  2. (42) is just an integer in parentheses; (42,) is a single-element tuple
  3. Use ..: let (first, .., last) = five_tuple;
  4. It returns the unit type ()

← PreviousNext →