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.