Rust’s iterators are lazy, meaning they don’t actually do anything until you ask them to produce a value.
Let’s see map in action. Imagine you have a list of numbers and you want to double each one.
let numbers = vec![1, 2, 3, 4, 5];
let doubled_iterator = numbers.iter().map(|x| x * 2);
// At this point, doubled_iterator is just a description of what to do.
// No multiplication has happened yet.
// To get the values, we need to consume the iterator, for example, by collecting them into a new vector:
let doubled_numbers: Vec<_> = doubled_iterator.collect();
println!("{:?}", doubled_numbers); // Output: [2, 4, 6, 8, 10]
Here, iter() creates an iterator over the elements of numbers. map(|x| x * 2) adapts this iterator, creating a new iterator that, for each element it receives, applies the closure |x| x * 2 and yields the result. collect() then pulls values from this adapted iterator one by one until it’s exhausted.
filter is similar, but it only lets elements pass through that satisfy a condition.
let numbers = vec![1, 2, 3, 4, 5, 6];
let even_numbers_iterator = numbers.iter().filter(|&x| x % 2 == 0);
// Again, no filtering has occurred yet.
let even_numbers: Vec<_> = even_numbers_iterator.collect();
println!("{:?}", even_numbers); // Output: [2, 4, 6]
The filter adapter takes a closure that returns a boolean. If the closure returns true for an element, that element is yielded by the new iterator. If it returns false, the element is skipped. The &x in the closure is important: filter passes a reference to the element to the closure. We dereference it implicitly with |&x| or explicitly with |x| **x % 2 == 0.
fold (also known as reduce in some other languages) is a powerful adapter that aggregates iterator elements into a single value.
let numbers = vec![1, 2, 3, 4, 5];
// Summing the numbers using fold
let sum = numbers.iter().fold(0, |acc, x| acc + x);
println!("Sum: {}", sum); // Output: Sum: 15
// You can also use it for more complex aggregations, like building a string
let words = vec!["hello", " ", "world"];
let sentence = words.iter().fold(String::new(), |mut acc, word| {
acc.push_str(word);
acc
});
println!("Sentence: {}", sentence); // Output: Sentence: hello world
fold takes two arguments: an initial value for the accumulator (0 in the sum example, String::new() in the sentence example) and a closure. The closure receives the current accumulator value (acc) and the next element from the iterator (x). It returns the new accumulator value. This process repeats until the iterator is exhausted, at which point the final accumulator value is returned.
The beauty of these adapters is their composability. You can chain them together to perform complex data transformations in a declarative and efficient way.
let data = vec!["1", "2", "three", "4", "five", "6"];
let processed_numbers: Vec<i32> = data.iter()
.filter_map(|s| s.parse::<i32>().ok()) // Try parsing, filter out errors
.map(|n| n * 2) // Double the valid numbers
.filter(|n| *n > 5) // Keep only those greater than 5
.collect();
println!("{:?}", processed_numbers); // Output: [8, 12]
Here, we first filter_map to attempt parsing each string into an i32. .ok() converts the Result from parse into an Option, so filter_map can filter out the None values (parse errors) and unwrap the Some values. Then, we map to double the numbers, filter to keep only those greater than 5, and finally collect.
What’s often missed is how the order of adapters matters, not just for the final result, but for performance. An adapter can short-circuit or perform work that is then discarded by a later adapter. For example, if you have a very large collection and you want to find the first element that matches a condition, find is your friend. find itself is a consuming adapter that uses filter internally but stops as soon as it finds a match.
let numbers = (0..1_000_000).map(|x| x * 2); // Imagine a huge, lazily generated sequence
// This is inefficient if you only need the first match:
// let first_match_inefficient = numbers.filter(|&x| x > 1_000_000).collect::<Vec<_>>()[0];
// This is efficient:
let first_match_efficient = numbers.find(|&x| x > 1_000_000);
println!("{:?}", first_match_efficient); // Output: Some(1000002)
In the inefficient example, collect() would iterate through the entire million-element sequence, create a vector, and then you’d access the first element. find, however, iterates only until it encounters the first element that satisfies the predicate x > 1_000_000, and then it stops immediately, returning Some(element). If no element matches, it returns None.
The next step is understanding how to implement your own custom iterator adapters.