rust-iteradores-map-filter-collect

Map, filter and collect in Rust: functional iterators

  • 4 min

Adapters and consumers of iterators are methods that transform a sequence or force its evaluation.

In the previous article we said that iterators are lazy. They are like a stopped assembly line. Today we are going to learn to:

  1. Modify the assembly line (add operations that transform or discard parts) with adapters.
  2. Flip the switch so that the line produces a result using a consumer.

This way of programming, chaining small operations, combines a fluent API with a functional style.

Adapters: transforming the sequence

Adapters are methods that take an iterator and return another different iterator with some added logic. Remember: nothing is executed yet, we are only setting up the pipeline.

map: one-to-one transformation

It takes each element and applies a closure to transform it into something else.

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

// Creates an iterator that will multiply by 2 when asked.
// The iterator type changes from Iter<i32> to Map<Iter<i32>, Closure>
let doubles = numbers.iter().map(|x| x * 2);
Copied!

filter: selection

It takes each element and decides whether to pass it based on a closure that returns bool (a predicate).

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

// Note: filter receives a REFERENCE to the element it iterates over.
// If iter() already gives references (&i32), filter receives (&&i32).
// That's why we use **x or destructure with |&x|
let even_numbers = numbers.iter().filter(|&x| x % 2 == 0);
Copied!

Other useful adapters

  • take(n): Takes only the first n elements and discards the rest.
  • skip(n): Skips the first n elements.
  • zip(other_iter): Combines two iterators into one that returns tuples (a, b).
  • enumerate(): Returns tuples (index, value).
let characters = vec!['a', 'b', 'c'];

for (i, c) in characters.iter().enumerate() {
    println!("At position {} we have the letter {}", i, c);
}
Copied!

Consumers: materializing the result

For all of the above to be useful, we need a method that consumes the iterator and generates a final value.

collect: the universal collector

It is the most important consumer. It transforms the iterator into a Collection (such as Vec, String, HashMap, etc.).

The problem is that collect is so generic that Rust often doesn’t know what you want to create. Do you want a Vec<i32>? A HashSet<i32>? A LinkedList?

Therefore, we almost always need to help the compiler. We have two ways:

Type annotation on the variable:

let doubles: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
Copied!

Turbofish syntax (::<>): It’s a funny name for an unusual syntax. We tell collect the explicit type.

let doubles = numbers.iter().map(|x| x * 2).collect::<Vec<i32>>();
Copied!

Other common consumers

  • sum(): Sums all elements (must be a numeric type).
  • find(predicate): Returns the first element that satisfies the condition (Option<T>). Short-circuits (stops iterating as soon as it finds it).
  • any() / all(): Return bool if any or all satisfy the condition.

Chaining operations

Now everything fits together. We can combine these pieces to perform complex operations in a very readable way.

Example: Imagine we have a list of numbers. We want to:

  1. Keep only those greater than 10.
  2. Multiply them by 3.
  3. Sum the total result.

Imperative style with a loop:

let numbers = vec![5, 12, 3, 20, 8];
let mut sum = 0;
for n in &numbers {
    if *n > 10 {
        let value = n * 3;
        sum += value;
    }
}
println!("Result: {}", sum);
Copied!

Functional style with iterators:

let numbers = vec![5, 12, 3, 20, 8];

let sum: i32 = numbers.iter()
    .filter(|&n| *n > 10) // 1. Filter
    .map(|n| n * 3)       // 2. Transform
    .sum();               // 3. Consume

println!("Result: {}", sum);
Copied!

The functional code describes WHAT we want to do, not HOW to do it step by step. It’s easier to read, harder to get wrong, and thanks to Rust’s optimizations, just as fast.