13.6 Practical Examples

Let’s see how iterators are used for typical programming tasks.

13.6.1 Processing Lines from a File Safely

Iterators shine when dealing with I/O, allowing robust handling of potential errors and easy data transformation.

// Objective: Read a file containing numbers (one per line), potentially
// mixed with invalid lines or empty lines, and sum the valid numbers.
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader};
use std::path::Path;

// Function to read file and sum valid numbers
fn sum_numbers_in_file(path: &Path) -> io::Result<i64> {
    let file = File::open(path)?; // Open file, ? propagates errors
    let reader = BufReader::new(file); // Use buffered reader for efficiency

    // Process lines using iterator chain
    let sum = reader.lines() // Produces an iterator yielding io::Result<String>
        .filter_map(|line_result| {
            // Stage 1: Handle potential I/O errors from reading lines
            line_result.ok() // Discard lines with I/O errors, keep Ok(String)
        })
        .filter_map(|line| {
            // Stage 2: Handle potential parsing errors
            line.trim().parse::<i64>().ok() // Trim whitespace, attempt parse, keep Ok(i64)
        })
        .sum(); // Sum the successfully parsed i64 values

    Ok(sum)
}

fn main() {
    let filename = "numbers_example.txt";
    let file_path = Path::new(filename);

    // Create a dummy file for the example using fs::write
    let content = "10\n20\n  \nthirty\n40\n-5\n invalid entry ";
    if let Err(e) = fs::write(file_path, content) {
        eprintln!("Failed to create dummy file: {}", e);
        return;
    }

    // Call the function and handle the result
    match sum_numbers_in_file(file_path) {
        Ok(total) => println!("Sum from file '{}': {}", filename, total),
        // Expected: 10 + 20 + 40 - 5 = 65
        Err(e) => eprintln!("Error processing file '{}': {}", filename, e),
    }

    // Clean up the dummy file (ignore potential error)
    let _ = fs::remove_file(file_path);
}

Here, filter_map elegantly handles two potential failure points in the pipeline: I/O errors during line reading (reader.lines() yields Result<String>) and parsing errors (parse() yields Result<i64>). The core logic remains concise and focused on the successful data transformations.

13.6.2 Functional-Style Data Transformation

Iterator chains allow complex data transformations to be expressed clearly and declaratively.

fn main() {
    let names = vec!["  alice ", " BOB", "   ", "charlie  ", "DAVID ", ""];

    let processed_names: Vec<String> = names
        .into_iter() // Consume the Vec<&str>, yields owned &str
        .map(|s| s.trim()) // Trim whitespace -> yields &str
        .filter(|s| !s.is_empty()) // Remove empty strings -> yields non-empty &str
        .map(|s| { // Convert to Title Case -> yields owned String
            let mut chars = s.chars();
            match chars.next() {
                None => String::new(), // Should not happen due to previous filter
                Some(first_char) => {
                    // Convert first char to uppercase, rest to lowercase
                    first_char.to_uppercase().collect::<String>()
                        + &chars.as_str().to_lowercase()
                }
            }
        })
        .collect(); // Collect the resulting Strings into a Vec<String>

    println!("Processed Names: {:?}", processed_names);
    // Output: Processed Names: ["Alice", "Bob", "Charlie", "David"]
}

This chain clearly expresses the steps: take ownership, trim whitespace, remove empty strings, convert to title case, and collect into a new vector. Each step is distinct and easy to understand.