map, filter, collect and chains
An adaptor takes an iterator and returns another iterator. map changes each item, filter drops some, take stops early, and the chain they build does no work until a consumer asks for values. That is why a long chain over a vector is as cheap as the loop it replaces.
The lesson covers the two adaptors a first program needs, proves the laziness with a counter, and compares a pipeline with the loop it would replace. The last section is the error for calling an adaptor on the collection instead of on its iterator.
map
map applies a closure to every item and yields the result. It is lazy, so the closure runs when the result is consumed; collect is the usual consumer, and it needs to know which collection to build.
The program doubles four values into a vector and prints it. The type annotation on the binding tells collect what to produce.
fn main() {
let values = vec![1, 2, 3, 4];
let doubled: Vec<i32> = values.iter().map(|value| value * 2).collect();
println!("{doubled:?}");
}
rustc --edition 2024 map.rs
./map
[2, 4, 6, 8]
The closure is the one from the lesson on closures, and the reference in the result is why collect::<Vec<i32>> works without a clone.
filter
filter keeps the items for which the closure returns true and drops the rest. The closure receives a reference to the item, so an integer has to be dereferenced before the remainder operator.
The program keeps the even numbers of one to six and prints the three references. The output is the debug form of a vector of references.
fn main() {
let values = vec![1, 2, 3, 4, 5, 6];
let even: Vec<&i32> = values.iter().filter(|value| *value % 2 == 0).collect();
println!("{even:?}");
}
rustc --edition 2024 filter.rs
./filter
[2, 4, 6]
Collecting references is one choice; copied or cloned would produce owned values instead. The lesson on iterators explains where the references come from.
Chains are lazy
A chain is a description, not a loop. The closure inside map runs once per item when the chain is consumed, and not before, which the counter below shows with a cell.
The program prints zero before the collect and three after it. The count is the proof that the mapping happened during the consumption.
use std::cell::Cell;
fn main() {
let values = vec![1, 2, 3];
let calls = Cell::new(0);
let chain = values.iter().map(|value| {
calls.set(calls.get() + 1);
value * 10
});
println!("before {}", calls.get());
let collected: Vec<i32> = chain.collect();
println!("after {} {collected:?}", calls.get());
}
rustc --edition 2024 lazy.rs
./lazy
before 0
after 3 [10, 20, 30]
A chain that is never consumed does no work at all, which is also a way to write a bug: a `map` whose result is dropped looks like it ran. The compiler helps with `#[must_use]` on the adaptors, which the lesson on assertions explains. Laziness is what lets take stop a million-element range after three items. The cell is the interior-mutability type from the lesson on const and mutability.
A pipeline
A pipeline chains filter, map and take before the sum. Each stage passes an iterator to the next, and the consumer at the end pulls one item at a time through the whole chain.
The program takes the odd values of an array, squares them and sums the first three. The output is ninety-one, which the loop version would compute with a counter and a break.
fn main() {
let values = [3, 8, 1, 9, 4, 7];
let total: i32 = values
.into_iter()
.filter(|value| value % 2 != 0)
.map(|value| value * value)
.take(3)
.sum();
println!("{total}");
}
rustc --edition 2024 pipeline.rs
./pipeline
91
The chain compiles to a loop with no intermediate vector: the compiler fuses the stages, and `take` stops the whole thing after three items. A loop that does the same needs a mutable total and a counter, which is longer and easier to get wrong at the boundaries. The lesson on folds shows the consumer that can replace the last stage with an accumulator.
A vector is not an iterator
Adaptors belong to the iterator, not to the collection. Calling map on a Vec directly is E0599, and the note explains that the method exists for types that implement Iterator.
The program below calls the adaptor on the vector. The fix is one iter(), or into_iter() when the values may be moved.
fn main() {
let values = vec![1, 2, 3];
let doubled = values.map(|value| value * 2);
println!("{doubled:?}");
}
rustc --edition 2024 map_err.rs
error[E0599]: the method `map` exists for struct `Vec<{integer}>`, but its trait bounds were not satisfied
--> map_err.rs:3:26
|
3 | let doubled = values.map(|value| value * 2);
| ^^^
|
= note: the following trait bounds were not satisfied:
`Vec<{integer}>: Iterator`
which is required by `&mut Vec<{integer}>: Iterator`
`[{integer}]: Iterator`
which is required by `&mut [{integer}]: Iterator`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0599`.
The note lists the unsatisfied bounds, which is the compiler walking from the method to the trait. The lesson on sorting uses the same distinction for methods that belong to the slice.
Exercises
- Turn a vector of words into a vector of their lengths with map and collect.
- Keep the words longer than three letters and print them with a for loop.
- Replace the pipeline with a loop and compare the two versions for the same array.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does an adaptor return?
Answer
Another iterator
Adaptors are lazy and return a new iterator; a consumer is what produces a value.
-
When does the closure inside map run?
Answer
When the chain is consumed
The chain is lazy, so the work happens as the consumer pulls items.
-
What does
collectneed to know?Answer
Which collection to build
The target type comes from the binding or a turbofish, because collect is generic.
-
What does filter keep?
Answer
The items for which the closure returns true
The predicate decides which items pass, and the order is preserved.
-
What does this program print?
fn main() { let values = vec![1, 2, 3, 4]; let doubled: Vec<i32> = values.iter().map(|value| value * 2).collect(); println!("{doubled:?}"); }Answer
[2, 4, 6, 8]
Every value is doubled and collected into a new vector.
-
What does this program print?
fn main() { let values = vec![1, 2, 3, 4, 5, 6]; let even: Vec<&i32> = values.iter().filter(|value| *value % 2 == 0).collect(); println!("{even:?}"); }Answer
[2, 4, 6]
The predicate keeps the three even numbers in order.
-
What does this program print?
use std::cell::Cell; fn main() { let values = vec![1, 2, 3]; let calls = Cell::new(0); let chain = values.iter().map(|value| { calls.set(calls.get() + 1); value * 10 }); println!("before {}", calls.get()); let collected: Vec<i32> = chain.collect(); println!("after {} {collected:?}", calls.get()); }Answer
before 0 / after 3 [10, 20, 30]
The counter stays at zero until the collect, which runs the closure three times.
-
What does this program print?
fn main() { let values = [3, 8, 1, 9, 4, 7]; let total: i32 = values .into_iter() .filter(|value| value % 2 != 0) .map(|value| value * value) .take(3) .sum(); println!("{total}"); }Answer
91
The odd values are three, one, nine, four and seven before the take, and their first three squares add to ninety-one.
-
What is the first line of the compiler output for this program?
fn main() { let values = vec![1, 2, 3]; let doubled = values.map(|value| value * 2); println!("{doubled:?}"); }Answer
error[E0599]: the method
mapexists for structVec<{integer}>, but its trait bounds were not satisfiedThe method belongs to Iterator, and a vector has to be turned into one with iter().
-
What does this program print?
fn main() { let values = vec![1, 2, 3]; let text: String = values.iter().map(|value| value.to_string()).collect(); println!("{text}"); }Answer
123
collect into a String concatenates the three strings, and the print shows 123.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev