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.