Transforming data with map and filter
You have a vector of strings representing user inputs. Some are empty. You need to trim the whitespace, filter out the blanks, convert the rest to uppercase, and store the result in a new vector. A for loop works, but it drags you into managing a temporary collection, checking lengths, and remembering to push. You end up writing ten lines of imperative code for a transformation that conceptually takes two steps. Rust gives you a better way. You chain filter and map to describe what you want, not how to do it.
The conveyor belt model
Think of an iterator as a conveyor belt carrying items one by one. filter is a quality control station. It looks at each item. If the item passes a test, it stays on the belt. If not, it gets kicked off. map is a transformation station. It takes every item that reaches it, applies a function, and puts the result back on the belt.
You can chain these stations. The belt doesn't start moving until you ask for the final result. This is lazy evaluation. The work happens on demand, item by item, without building intermediate collections. You define the pipeline, and the data flows through only when you pull the lever.
Minimal example
Here is the basic pattern. You start with a collection, create an iterator, chain adapters, and consume the result.
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// iter() creates an iterator yielding references to the elements.
let result: Vec<i32> = numbers
.iter()
// filter() keeps only items where the closure returns true.
// We use &&n to destructure the reference from iter() and get the value.
.filter(|&&n| n % 2 == 0)
// map() transforms each remaining item using the closure.
// The closure receives a reference, so we dereference it to multiply.
.map(|&n| n * 2)
// collect() consumes the iterator and builds a Vec from the results.
// Without this, the chain does nothing.
.collect();
println!("{:?}", result);
}
The output is [4, 8, 12]. The odd numbers never reach map. The multiplication never happens for discarded items. Memory usage stays flat.
What happens under the hood
When you write this chain, the compiler doesn't create a new vector for the filtered numbers, nor does it create one for the mapped numbers. filter returns an adapter struct. map returns another adapter. These structs hold the state of the chain.
When you call collect, the Vec implementation asks the iterator for the next item. The request ripples backward. map asks filter for an item. filter asks the original slice for an item. If filter rejects it, it asks for another. If it accepts, it passes it to map. map transforms it and returns it to collect. This happens one item at a time.
The compiler inlines these calls. The generated assembly often looks identical to a hand-written loop, but with less chance of off-by-one errors or forgotten pushes. You get the safety of declarative code with the performance of imperative code. Trust the chain. The optimizer handles the rest.
Realistic scenario
In real code, you often work with structs. Here is a pattern for processing a list of users.
#[derive(Debug)]
struct User {
name: String,
age: u8,
}
/// Filters for adults and returns formatted summary strings.
fn get_adult_reports(users: &[User]) -> Vec<String> {
users
.iter()
// filter() removes minors before any formatting cost.
.filter(|u| u.age >= 18)
// map() creates a new String for each remaining user.
.map(|u| format!("{} is an adult", u.name))
// collect() gathers the strings into a Vec.
.collect()
}
fn main() {
let users = vec![
User { name: "Alice".into(), age: 15 },
User { name: "Bob".into(), age: 25 },
User { name: "Charlie".into(), age: 30 },
];
let reports = get_adult_reports(&users);
println!("{:?}", reports);
}
Put filter before map when possible. If the filter drops many items, you avoid the cost of transformation on discarded data. The compiler can sometimes reorder operations, but don't rely on it. Write the logic that minimizes work.
Navigating the reference chain
The syntax |&&n| and |&n| trips up beginners. It comes from how iterators yield references.
iter() yields &T. When filter calls the underlying iterator, it gets &T. It then passes a reference to that item to your closure. So your closure receives &&T. You destructure with |&&n| to get n as T.
map receives the item from filter. If filter accepted &&T, map receives &T. So map uses |&n|.
This pattern is consistent. iter() gives references. Adapters pass references to closures. Destructure the references you don't need. The compiler will tell you if you get it wrong.
If you use into_iter() instead of iter(), the iterator yields owned values. The closures receive T directly. No double references. Choose iter() when you need to keep the original collection. Choose into_iter() when you want to move the values out.
Common pitfalls
The silent chain
If you forget collect, the code compiles and runs, but nothing happens. The iterator is created and dropped immediately. You get no error. You just get an empty result or no side effects. Always end your chain with a consumer like collect, for_each, or count. An unconsumed iterator is a wasted computation.
Type inference failures
collect can build many types. The compiler often needs a hint.
let result = numbers.iter().map(|n| n * 2).collect();
The compiler rejects this with E0282: type annotations needed. It sees collect and asks, "Collect into what? A Vec? A HashSet? A String?" Add the type annotation: let result: Vec<i32> = ....
Moving out of borrowed content
If you try to move a value out of a reference in map, you hit a borrow error.
let result: Vec<String> = users.iter().map(|u| u.name).collect();
The compiler rejects this with E0507: cannot move out of borrowed content. You have a &User, so u.name is a &String. You can't move the String out. You need .map(|u| u.name.clone()) or use .into_iter() if you own the vector and don't need it later.
Side effects in map
map is for transformation. If you just want to print, use for_each. Using map for side effects is a code smell. The return value of map is ignored, which suggests you didn't mean to transform anything.
// Bad: map returns a new iterator that is immediately dropped.
numbers.iter().map(|n| println!("{}", n));
// Good: for_each consumes the iterator and performs the action.
numbers.iter().for_each(|n| println!("{}", n));
When to use what
Use map when you need to transform every item in a collection to a new value. Use filter when you need to discard items based on a condition. Use filter_map when you need to filter and transform at the same time, especially when the transformation might fail and return None. Reach for a for loop when you have complex logic that doesn't fit a single expression, or when you need to perform side effects like printing or mutating external state. Use into_iter when you want to consume the collection and move the values out. Use iter when you only need to read the values and keep the original collection alive.
Prefer chains for data transformation. Reach for loops when the logic gets tangled.