How to Use map, filter, and collect in Rust

Transform, filter, and collect Rust data using iterator methods in a single chain.

The conveyor belt of data

You are building a leaderboard for a local arcade game. The sensor dumps a list of raw scores into memory. Some are glitches that read as negative numbers. You need to keep only the valid scores, double them for a weekend bonus, and store the final list to send to the server.

In Python, you would write a list comprehension. In Rust, you chain methods on an iterator. The pattern is iter, filter, map, collect. This chain processes data functionally without manual loops or temporary variables.

Think of an iterator as a conveyor belt. filter is the quality control inspector standing next to the belt, throwing bad items into a bin. map is the robot arm that paints or resizes every item that passes inspection. collect is the box at the end of the line where you pack the finished goods.

Crucially, the conveyor belt is lazy. Nothing moves until you open the box at the end. Calling filter and map just sets up the machinery. The work happens only when you call collect or another consumer method.

Minimal example: Filtering and transforming

Here is the arcade score scenario in code. The vector holds raw integers. We borrow the vector, filter out negatives, double the rest, and collect the results.

fn main() {
    let scores = vec![10, -5, 20, 30, -10, 40];

    let bonus_scores: Vec<i32> = scores
        .iter()
        // Filter out negative glitches. The closure receives &i32.
        .filter(|&&score| score >= 0)
        // Double the valid scores. The closure receives &i32.
        .map(|&score| score * 2)
        // Gather the results into a new Vec.
        .collect();

    println!("{:?}", bonus_scores); // [20, 40, 60, 80]
}

The type annotation Vec<i32> on bonus_scores is required. The compiler cannot guess the target collection type from collect alone. It could be a Vec, a HashSet, or a String. You must provide the hint.

How the chain executes

When you call .iter(), Rust creates an iterator that borrows the vector. It does not copy any data. The iterator holds a reference to the vector and an index pointing to the current position.

Calling .filter() and .map() adds adapters to the chain. Each adapter wraps the previous iterator and stores a closure. The chain remains lazy. No items are processed yet.

The moment you call .collect(), the iterator starts pulling items. For each item, the chain runs backward through the adapters. filter executes its closure. If the closure returns true, the item passes to map. map executes its closure and produces a new value. collect takes that value and pushes it into the target collection.

This lazy evaluation enables powerful patterns. You can chain adapters on infinite sequences as long as you consume a finite number of items.

fn main() {
    // Generate an infinite sequence of integers starting at 1.
    let evens: Vec<i32> = (1..)
        .filter(|&n| n % 2 == 0)
        .take(5)
        .collect();

    println!("{:?}", evens}; // [2, 4, 6, 8, 10]
}

The take(5) adapter limits the stream to five items. Without it, collect would loop forever. Laziness means the infinite generator only produces values on demand.

The closure syntax trap

Beginners often stumble on the closure parameters in filter and map. The syntax depends on whether the iterator yields values or references.

The method .iter() yields references. If your vector holds i32, the iterator yields &i32. The closure parameter receives &i32. If you write |n|, the variable n has type &i32. You cannot perform arithmetic on a reference directly.

You have two options. You can dereference manually with *n, or you can use pattern matching in the closure signature. The pattern |&n| tells Rust to match the reference and extract the inner value. The variable n then has type i32.

let numbers = vec![1, 2, 3];

// n is &i32 here. This fails to compile.
// let doubled = numbers.iter().map(|n| n * 2).collect();

// n is i32 here. This compiles.
let doubled: Vec<i32> = numbers.iter().map(|&n| n * 2).collect();

If you forget the & in the pattern and try to use n as a value, the compiler rejects the code with E0308 (mismatched types). It expects an integer but finds a reference.

The convention is to use |&n| for simple types like integers and floats when using .iter(). It keeps the closure body clean. For complex structs, you might prefer |item| and access fields via item.field. The compiler helps you find the right shape.

Realistic example: Processing user data

Real code rarely deals with plain integers. You usually process structs. Consider a service that ingests user inputs. The inputs are raw strings with whitespace. You need to trim them, convert to uppercase, and discard empty strings.

struct UserInput {
    raw: String,
    timestamp: u64,
}

fn process_inputs(inputs: Vec<UserInput>) -> Vec<String> {
    inputs
        .into_iter()
        // Transform each input into a cleaned string.
        .map(|input| input.raw.trim().to_uppercase())
        // Discard empty results.
        .filter(|text| !text.is_empty())
        .collect()
}

fn main() {
    let inputs = vec![
        UserInput { raw: "  hello ".to_string(), timestamp: 100 },
        UserInput { raw: "".to_string(), timestamp: 101 },
        UserInput { raw: "  world ".to_string(), timestamp: 102 },
    ];

    let cleaned = process_inputs(inputs);
    println!("{:?}", cleaned); // ["HELLO", "WORLD"]
}

This example uses .into_iter() instead of .iter(). The method into_iter consumes the vector and yields owned values. The closure in map receives UserInput by value. It can move the raw field out of the struct.

If you used .iter(), the closure would receive &UserInput. You could not move raw out of a reference. The compiler would reject the code with E0507 (cannot move out of borrowed content). You would be forced to clone the string, which wastes memory.

Use into_iter when you own the collection and do not need it afterwards. It avoids clones and gives you full ownership of the items.

Convention aside: You will see collect::<Vec<_>>() in many codebases. The turbofish syntax provides the type hint inline. It is a convention to keep collect calls readable when the type is obvious from the surrounding context. It saves vertical space compared to a separate type annotation.

Pitfalls and compiler errors

Iterators are safe, but the type system enforces strict rules. You will encounter errors when the chain shape does not match the data shape.

Type inference fails on collect. The method collect is generic over the output type. If you do not provide a type hint, the compiler cannot resolve the generic parameter. You get E0283 (type annotations needed). The error message lists possible types like Vec<T>, HashSet<T>, and String. Add a type annotation to the variable or use the turbofish syntax.

Moving out of borrowed content. You cannot move data out of a reference. If you use .iter() and your closure tries to take ownership of the item, the compiler rejects the code with E0507. The fix is to switch to .into_iter() if you own the data, or to clone the item if you must keep the original. Cloning is expensive; prefer ownership transfer when possible.

Filtering before mapping costs cycles. Order matters for performance. If map is expensive, put filter first. Filtering early discards items before the transformation runs. If you map first, you waste computation on items you will throw away.

// Inefficient: maps every item, then filters.
let result = data.iter().map(|x| expensive_transform(x)).filter(|x| x.is_valid()).collect();

// Efficient: filters first, then maps only valid items.
let result = data.iter().filter(|x| x.is_valid()).map(|x| expensive_transform(x)).collect();

Filter early, map late. Save cycles on data you are going to throw away.

Decision: Choosing the right adapter

Rust provides several iterator adapters. Pick the one that matches the shape of your data.

Use map when you need to transform every item in a sequence. Use filter when you need to discard items based on a condition. Use collect when you need to materialize an iterator into a concrete collection. Use filter_map when you need to filter and transform in one step, especially when the transformation might fail and return None. Use into_iter when you want to consume the collection and take ownership of the items. Use iter when you only need to read the items and keep the original collection alive. Use enumerate when you need the index alongside each item. Use flat_map when your transformation produces multiple items or nested collections that need to be flattened.

Pick the adapter that matches the shape of your data, not the shape of your Python habits. The compiler will guide you to the right choice.

Where to go next