map, filter, and collect

Most real programs spend their time transforming collections of data: turning raw input into something usable, throwing away the entries that don’t matter, and building a new collection out of what’s left. Rust gives you three tools that handle almost all of this: map, which transforms every element; filter, which keeps only the elements that pass a test; and collect, which gathers the results back into a concrete collection like a Vec. Together they let you write data pipelines that read almost like plain English, while the compiler still checks every type and every borrow along the way.

Overview: How Iterators, map, filter, and collect Work

Anything you can loop over in Rust implements the Iterator trait, whose only required method is fn next(&mut self) -> Option<Self::Item>. Calling next() repeatedly hands back Some(item) until the sequence runs out, at which point it returns None. A for loop is really just sugar for calling next() in a loop until it returns None.

map and filter are called iterator adaptors. Calling one of them does not loop over anything by itself — it wraps the iterator you called it on inside a new, small struct (literally named Map and Filter in the standard library) that remembers what to do once somebody actually asks for the next value. Think of building a chain like numbers.iter().filter(...).map(...) as drawing a blueprint for a factory conveyor belt: describing the stations does not move a single item down the line. Nothing runs until you call a consumer — a method like collect, sum, count, or a plain for loop — which is the equivalent of switching the belt on. This is called lazy evaluation, and it means an unused chain of adaptors does nothing at all (the compiler will even warn you about it).

map(closure) takes an iterator over some Item type and produces a new iterator over whatever type the closure returns, applying the closure to one element at a time, on demand. It can never add or remove elements — only change what each one contains. filter(predicate), where the predicate is a closure returning bool, keeps only the elements for which the predicate returns true and silently drops the rest, without changing their type. Note that filter‘s closure receives a reference to each item (&Self::Item), because it only needs to inspect the value to make a decision, not consume it — this is why filter closures often need an extra & in their pattern compared to map closures.

collect is the consumer that finally pulls every item through the chain and rebuilds a real collection. It is generic over its return type through the FromIterator trait, which is implemented for Vec<T>, String, HashMap<K, V>, HashSet<T>, and more. Because many types implement FromIterator, the compiler usually cannot guess which one you want just from the chain itself — you almost always need to say so explicitly, either with a type annotation on the variable or with the “turbofish” syntax .collect::<Vec<i32>>().

Ownership matters here too, because it decides how you start the chain. .iter() produces an iterator of shared references (&T) and borrows the collection, leaving it usable afterward. .into_iter() produces an iterator of owned values (T) and moves the collection, so the original binding can no longer be used. .iter_mut() produces mutable references (&mut T). Choosing the right one up front avoids most of the ownership errors beginners hit with iterator chains.

Syntax

The general shape of a map/filter/collect pipeline looks like this:

iterator.map(|item| expression).collect::<CollectionType>();
iterator.filter(|item| condition).collect::<CollectionType>();
iterator.filter(|item| condition).map(|item| expression).collect::<CollectionType>();
Part Meaning
iterator Anything implementing Iterator, usually from .iter(), .iter_mut(), or .into_iter() on a collection.
.map(|item| expression) Transforms each item; the closure’s return value becomes the new item type.
.filter(|item| condition) Keeps items where condition is true; the closure receives &item.
.collect::<CollectionType>() Consumes the iterator and builds CollectionType, which must implement FromIterator.

A few other iterator methods you’ll see alongside these three:

Method What it does
filter_map Combines filter and map: the closure returns Option<B>, and None results are dropped.
enumerate Pairs each item with its index, yielding (usize, Item).
take(n) / skip(n) Keeps only the first n items, or drops the first n items.
sum / count Consumers that add up numeric items, or count how many items there are.
fold A general-purpose consumer that reduces the iterator to a single value using an accumulator.

Examples

Example 1: Doubling numbers with map

The simplest possible pipeline: borrow each element, transform it, collect the results into a new Vec.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
    println!("{:?}", doubled);
}

Output:

[2, 4, 6, 8, 10]

numbers.iter() yields references (&i32) without taking ownership of numbers, so it is still usable afterward. The closure |n| n * 2 receives each reference, multiplies it, and returns a plain i32; map produces a new iterator of those i32 values, and collect — guided by the Vec<i32> annotation — gathers them into a fresh Vec.

Example 2: Keeping only even numbers with filter

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let evens: Vec<i32> = numbers.iter().filter(|&&n| n % 2 == 0).cloned().collect();
    println!("{:?}", evens);
}

Output:

[2, 4, 6, 8, 10]

numbers.iter() yields &i32, so filter‘s closure receives &&i32 — a reference to a reference. The pattern |&&n| unwraps both layers so n is a plain i32 we can compare with % 2 == 0. filter does not change the item type, so the result is still an iterator of &i32; .cloned() copies each surviving reference into an owned i32 before collect builds the final Vec.

Example 3: Filtering and mapping struct data together

Real code usually filters and maps in the same pipeline — here, keeping only well-paid employees and pulling out just their names.

struct Employee {
    name: String,
    salary: u32,
}

fn main() {
    let employees = vec![
        Employee { name: String::from("Alice"), salary: 72000 },
        Employee { name: String::from("Bob"), salary: 54000 },
        Employee { name: String::from("Carol"), salary: 91000 },
    ];

    let high_earners: Vec<String> = employees
        .iter()
        .filter(|e| e.salary > 60000)
        .map(|e| e.name.clone())
        .collect();

    println!("{:?}", high_earners);
}

Output:

["Alice", "Carol"]

employees.iter() borrows each Employee. filter drops Bob because 54000 > 60000 is false, leaving Alice and Carol. Only then does map run, cloning just the name field of the two survivors into owned Strings. Putting filter before map here means the discarded employee’s name is never cloned at all — wasted work is skipped, not just its result.

How It Works Step by Step

Because adaptors are lazy, a chain runs one element at a time, pulled through the whole pipeline by whatever is at the end, rather than one stage finishing over the whole collection before the next stage starts. This example makes that visible by printing inside the closure:

fn main() {
    let numbers = vec![1, 2, 3];
    let iter = numbers.iter().map(|n| {
        println!("processing {}", n);
        n * 2
    });
    println!("iterator created, nothing has run yet");
    let result: Vec<i32> = iter.collect();
    println!("result: {:?}", result);
}

Output:

iterator created, nothing has run yet
processing 1
processing 2
processing 3
result: [2, 4, 6]

Step by step: (1) numbers.iter() builds a small struct holding a pointer into numbers and a position. (2) .map(...) wraps that in a Map struct holding the inner iterator plus the closure — still nothing has executed, which is why the “nothing has run yet” line prints before any “processing” line. (3) .collect() starts calling next() on the outer Map, which calls next() on the inner iterator to get one &i32, runs the closure on it (printing and doubling), and hands the result back — one element completes the entire pipeline before the next one starts. (4) Once the inner iterator’s next() returns None, collect stops and the new Vec owns every produced value.

Common Mistakes

Mistake 1: Forgetting to Tell collect What to Build

collect cannot be called blindly — without a hint, the compiler has no way to know which of many possible FromIterator implementations you want.

fn main() {
    let numbers = vec![1, 2, 3, 4];
    let squares = numbers.iter().map(|n| n * n).collect();
    println!("{:?}", squares);
}

This fails with error[E0282]: type annotations needed for `Vec<_>`, because squares has no declared type and println!‘s {:?} doesn’t constrain it either. Fix it by annotating the variable (or using turbofish, .collect::<Vec<i32>>()):

fn main() {
    let numbers = vec![1, 2, 3, 4];
    let squares: Vec<i32> = numbers.iter().map(|n| n * n).collect();
    println!("{:?}", squares);
}

Output:

[1, 4, 9, 16]

Mistake 2: Using a Vec After into_iter() Has Moved It

into_iter() takes ownership of the collection, not a borrow of it — the original binding becomes invalid.

fn main() {
    let names = vec![String::from("Ana"), String::from("Bo")];
    let upper: Vec<String> = names.into_iter().map(|n| n.to_uppercase()).collect();
    println!("{:?}", names);
}

This fails with error[E0382]: borrow of moved value: `names`, because names.into_iter() moved every String out of the vector (and the vector itself) into the pipeline. Since we only needed to read each name, borrowing instead of moving fixes it:

fn main() {
    let names = vec![String::from("Ana"), String::from("Bo")];
    let upper: Vec<String> = names.iter().map(|n| n.to_uppercase()).collect();
    println!("{:?} {:?}", names, upper);
}

Output:

["Ana", "Bo"] ["ANA", "BO"]

Mistake 3: Mutably Borrowing a Collection While Iterating Over It

The borrow checker enforces “one mutable reference OR any number of immutable references” even across an iterator chain — you cannot push into a Vec from inside a closure that is also borrowing it for iteration.

fn main() {
    let mut numbers = vec![1, 2, 3];
    let doubled: Vec<i32> = numbers.iter().map(|n| {
        numbers.push(n * 2);
        n * 2
    }).collect();
    println!("{:?}", doubled);
}

This fails with error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable: numbers.iter() holds an immutable borrow for the whole chain, and numbers.push(...) inside the closure needs a mutable borrow at the same time. The fix is to keep reading and writing as two separate steps:

fn main() {
    let mut numbers = vec![1, 2, 3];
    let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
    numbers.extend(doubled.iter());
    println!("{:?}", numbers);
}

Output:

[1, 2, 3, 2, 4, 6]

Best Practices

  • Use .iter() when you still need the original collection afterward, and .into_iter() when you’re done with it and want to avoid unnecessary clones.
  • Always give collect an explicit target type, via a variable annotation or turbofish (.collect::<Vec<_>>()), rather than leaving it ambiguous.
  • Put .filter() before .map() in a chain when possible, so you never transform elements that are about to be thrown away.
  • Prefer a chain of adaptors ending in one collect() over manually pushing into a Vec inside a loop — it’s shorter and compiles to equally efficient code.
  • Reach for filter_map instead of a separate .filter() and .map() when the predicate and the transformation are really the same operation, such as parsing and keeping only the values that parsed successfully.
  • Remember that adaptors are lazy: a chain that’s built but never handed to collect, a for loop, or another consumer does nothing, and the compiler will warn about the unused iterator.

Practice Exercises

  1. Given let words = vec!["hi", "hello", "hey", "greetings"];, use filter and collect to build a Vec<&str> containing only the words with more than 3 characters. Expected output: ["hello", "greetings"].
  2. Given a Vec<i32> of Celsius temperatures, use map and collect to build a Vec<f64> of the same temperatures converted to Fahrenheit using f = c as f64 * 9.0 / 5.0 + 32.0.
  3. Starting from the numbers 1 through 20, chain filter and map to build a Vec<i32> containing the squares of only the numbers divisible by 3. Hint: filter for divisibility first, then square what’s left.

Summary

  • map transforms every element of an iterator by applying a closure; it never changes how many elements there are.
  • filter keeps only the elements for which a predicate closure returns true, and its closure receives a reference to each item.
  • collect is the consumer that finally runs the pipeline and gathers the results into a concrete type such as Vec<T>, String, or HashMap<K, V>, chosen through a type annotation or turbofish.
  • Iterator adaptors are lazy — nothing happens until a consuming method like collect, sum, or a for loop pulls values through the chain, one element at a time.
  • Use .iter() to borrow elements when you still need the original collection, and .into_iter() when you want to move ownership of the elements into the pipeline.
  • The borrow checker enforces the same ownership and borrowing rules inside iterator chains as everywhere else in Rust, so a closure that tries to mutate a collection it’s also iterating over will fail to compile.