5/5
## Mastering Iterators in Rust: `map`, `filter`, and `collect` Welcome to this lesson on leveraging common iterator functions in Rust. We'll explore `map`, `filter`, and `collect`, building upon your existing knowledge of how iterators are created. These tools are fundamental for writing expressive, efficient, and idiomatic Rust code for data processing. ## A Quick Recap: Creating Iterators Before diving into iterator adapters, let's briefly recall the three primary methods for obtaining an iterator from a collection in Rust: 1. **`into_iter()`**: This method consumes the collection it's called on. It takes ownership of the data and yields owned values of type `T`. Once `into_iter()` is used, the original collection can no longer be accessed. 2. **`iter()`**: This method borrows the collection immutably. It yields immutable references to the items within the collection, specifically of type `&T`. The original collection remains accessible after creating the iterator. 3. **`iter_mut()`**: This method borrows the collection mutably. It yields mutable references to the items, of type `&mut T`, allowing you to modify the elements of the collection in place. The original collection is mutably borrowed for the lifetime of the iterator. Understanding which of these to use is crucial as it dictates the type of data your iterator will yield and, consequently, how you'll interact with it in subsequent operations. ## Core Iterator Adapters: `map` and `filter` Iterator adapters are methods that transform an iterator into a new iterator. They are "lazy," meaning they don't perform any work until a consuming method is called. Two of the most frequently used adapters are `map` and `filter`. * **`map`**: The `map` adapter transforms each element of an iterator into a new element by applying a given closure. For an iterator yielding items of type `A`, `map` takes a closure `Fn(A) -> B` and produces a new iterator that yields items of type `B`. * **`filter`**: The `filter` adapter creates a new iterator that yields only those elements for which a given closure (often called a predicate) returns `true`. The closure must be of type `Fn(&Item) -> bool`, where `Item` is the type of element the iterator yields. The resulting iterator yields elements of the same type as the original iterator. ## Consuming Iterators: The `collect` Method While adapters like `map` and `filter` create new iterators, they don't actually execute the iteration or produce a final result. For that, we need a consuming adapter. The most versatile consuming adapter is `collect`. * **`collect`**: This method gathers all items from an iterator and assembles them into a specified collection, such as a `Vec`, `HashMap`, `String`, or any other type that implements the `FromIterator` trait. A crucial aspect of `collect` is that Rust often needs a type annotation to determine the target collection type, as `collect` is generic and can produce many different kinds of collections. ## Practical Examples: Transforming and Collecting Data Let's explore how these functions work together through practical examples. ### Example 1: `map` and `collect` with `Vec<u32>` This example demonstrates how to transform elements in a vector of numbers and collect them into a new vector. 1. **Initial Setup**: We start with a vector of unsigned 32-bit integers. ```rust // fn main() { let vals: Vec<u32> = vec![1, 2, 3, 4, 5]; // ... // } ``` 2. **Creating an Iterator and Using `map`**: We call `vals.iter()` to get an iterator that yields immutable references (`&u32`) to the items in `vals`. Then, we use `map` to increment each number. The closure `|x: &u32| *x + 1` takes a reference `x` of type `&u32`. We dereference `x` using `*x` to get the `u32` value and then add 1. Note: For `Copy` types like `u32`, Rust's auto-dereferencing allows `x + 1` to work as well, but `*x + 1` is more explicit about the dereferencing operation. 3. **Using `collect`**: The `collect()` method is called on the iterator returned by `map`. The type annotation `let v2: Vec<u32> = ...` informs `collect` that we want to gather the results into a new `Vec<u32>`. ```rust // fn main() { // let vals: Vec<u32> = vec![1, 2, 3, 4, 5]; // // map takes a closure: f(x: &u32) -> u32 // // The iterator from vals.iter() yields items of type &u32 // let v2: Vec<u32> = vals.iter().map(|x: &u32| *x + 1).collect(); // println!("v2 {:?}", v2); // } ``` If you run this (within a `main` function), the output will be: `v2 [2, 3, 4, 5, 6]` **A Note on Closures** Closures are anonymous functions you can store in a variable or pass as arguments to other functions. * **Syntax**: `|input_params| body_expression` for a single expression, or `|input_params| { /* multi-line body */ }` for more complex logic. * **Type Inference**: Rust's compiler is often able to infer the types of closure parameters and their return values from the context. However, explicit type annotations, as seen with `|x: &u32|`, can improve clarity and are sometimes necessary. * **Curly Braces `{}`**: Optional for single-expression bodies; required for multi-line bodies or when a block is needed for scope. ### Example 2: Versatility of `collect` - `Vec` vs. `HashMap` This example showcases how `collect()` can be used to create different collection types from the same transformed iterator data. 1. **Initial Setup**: We'll use a vector of tuples. ```rust use std::collections::HashMap; // Import HashMap // fn main() { let vals: Vec<(&str, u32)> = vec![("a", 1), ("b", 2), ("c", 3)]; // ... // } ``` 2. **Transforming and Collecting into `Vec<(String, u32)>`**: We iterate over `vals` using `iter()`, which yields `&(&str, u32)`. The `map` closure `|v| (v.0.to_string(), v.1 + 1)` transforms each tuple: * `v.0` (which is `&str`) is converted to an owned `String` using `to_string()`. * `v.1` (which is `u32`) is incremented by 1. The result is collected into a new `Vec<(String, u32)>`. ```rust // fn main() { // use std::collections::HashMap; // let vals: Vec<(&str, u32)> = vec![("a", 1), ("b", 2), ("c", 3)]; // // let v: Vec<(String, u32)> = vals.iter().map(|v| (v.0.to_string(), v.1 + 1)).collect(); // println!("vec {:?}", v); // } ``` Output: `vec [("a", 2), ("b", 3), ("c", 4)]` 3. **Transforming and Collecting into `HashMap<String, u32>`**: Using the exact same `vals.iter().map(...)` chain, we produce an iterator of `(String, u32)` tuples. This time, by changing the type annotation for `collect()`, we gather these key-value pairs into a `HashMap<String, u32>`. ```rust // fn main() { // use std::collections::HashMap; // let vals: Vec<(&str, u32)> = vec![("a", 1), ("b", 2), ("c", 3)]; // // // ... (previous Vec collection) // // let v_map: HashMap<String, u32> = vals.iter().map(|v| (v.0.to_string(), v.1 + 1)).collect(); // println!("hash map {:?}", v_map); // } ``` Output (order in HashMaps is not guaranteed): `hash map {"c": 4, "a": 2, "b": 3}` The key takeaway here is the power and flexibility of `collect`. The same iterator transformation logic can populate different kinds of collections, abstracting away the specific insertion mechanisms (like `push` for `Vec` or `insert` for `HashMap`). ## Chaining Iterator Adapters for Powerful Pipelines Iterator adapters can be chained together, allowing you to create sophisticated data processing pipelines in a very readable and declarative manner. ### Example 3: `filter` then `map` with `iter()` This example demonstrates filtering elements before transforming them. 1. **Initial Setup**: ```rust // fn main() { let vals: Vec<u32> = vec![1, 2, 3, 4, 5]; // ... // } ``` 2. **Chaining Operations**: * `vals.iter()`: Creates an iterator yielding `&u32` (references). * `.filter(|x: &&u32| **x <= 3)`: * The `filter` adapter takes a closure. Since `vals.iter()` yields `&u32` (let's call this `Item`), the closure for `filter` receives a reference to this item, `&Item`, which becomes `&&u32`. * `**x` performs a double dereference: the first `*` dereferences `&&u32` to `&u32`, and the second `*` dereferences `&u32` to `u32` for the comparison. * This filter keeps only elements whose values are less than or equal to 3. * `.map(|x: &u32| *x + 1)`: * The `map` closure receives `x` of type `&u32`. This is because `filter` passes through items of the original iterator's item type (`&u32` in this case) if they satisfy the predicate. * `*x + 1` dereferences the `&u32` to `u32` and increments the value. * `.collect()`: Gathers the results into a `Vec<u32>`. ```rust // fn main() { // let vals: Vec<u32> = vec![1, 2, 3, 4, 5]; // // let v_filtered_mapped: Vec<u32> = vals // .iter() // .filter(|x: &&u32| **x <= 3) // x is &&u32 // .map(|x: &u32| *x + 1) // x is &u32 // .collect(); // println!("filter -> map {:?}", v_filtered_mapped); // } ``` Output: `filter -> map [2, 3, 4]` Data flow: * Original `vals`: `[1, 2, 3, 4, 5]` * `iter()` yields: `&1, &2, &3, &4, &5` * `filter (value <= 3)` passes: `&1, &2, &3` * `map (value + 1)` transforms to: `2, 3, 4` * `collect()` creates: `[2, 3, 4]` ### Example 4: `filter` then `map` with `into_iter()` Let's see how the types change when using `into_iter()`, which moves ownership. 1. **Initial Setup**: We'll create a new vector to demonstrate `into_iter` consuming it. ```rust // fn main() { let vals_for_into_iter: Vec<u32> = vec![1, 2, 3, 4, 5]; // ... // } ``` 2. **Chaining Operations with `into_iter()`**: * `vals_for_into_iter.into_iter()`: Creates an iterator yielding owned `u32` values. This consumes `vals_for_into_iter`. * `.filter(|x: &u32| *x <= 3)`: * Since `into_iter()` yields `u32` (let's call this `Item`), the `filter` closure receives `&Item`, which is `&u32`. * `*x` dereferences `&u32` to `u32` for the comparison. * `.map(|x: u32| x + 1)`: * The `map` closure now receives `x` as an owned `u32` value, because `filter` passes through items of type `u32` (the `Item` type of the iterator from `into_iter`). * No dereference is needed for `x + 1` as `x` is already a `u32`. * `.collect()`: Gathers results into a `Vec<u32>`. ```rust // fn main() { // let vals_for_into_iter: Vec<u32> = vec![1, 2, 3, 4, 5]; // // let v_into_filtered_mapped: Vec<u32> = vals_for_into_iter // .into_iter() // .filter(|x: &u32| *x <= 3) // x is &u32 (reference to the owned u32) // .map(|x: u32| x + 1) // x is u32 (owned value) // .collect(); // println!("into_iter filter -> map {:?}", v_into_filtered_mapped); // // Note: vals_for_into_iter is moved here and cannot be used afterwards // } ``` Output: `into_iter filter -> map [2, 3, 4]` The result is the same, but the types handled by the closures differ due to `into_iter()` yielding owned values instead of references. ## Key Takeaways: Understanding Iterator Behavior * **Iterators are Lazy**: Adapters like `map` and `filter` don't perform their operations immediately. They construct a new iterator that represents the sequence of operations. The actual work is only executed when a consuming method like `collect()` is called. This laziness can lead to performance optimizations, as unnecessary intermediate collections might be avoided. * **Power of Chaining**: Iterator adapters can be elegantly chained together, creating expressive and concise data processing pipelines. This often leads to more readable code compared to manual loops with conditional logic. * **Role of Rust's Type System**: The strong type system in Rust, combined with type inference, plays a vital role. While types are often inferred, explicit type annotations (especially for the return type of `collect()`, and sometimes for closure parameters) are crucial for clarity and guiding the compiler. * **Ownership and Borrowing Impact**: Your choice between `iter()`, `iter_mut()`, and `into_iter()` directly influences whether your closures operate on references (`&T`, `&mut T`) or owned values (`T`). This, in turn, affects how you access and manipulate data within those closures (e.g., needing to dereference references). ## Conclusion Rust's iterators, along with adapters like `map` and `filter`, and consumers like `collect`, provide a powerful, efficient, and idiomatic way to work with collections. By understanding how these components interact, especially concerning types, ownership, and laziness, you can write highly declarative and effective Rust code for a wide range of data manipulation tasks. This functional approach often leads to cleaner, more maintainable, and less error-prone programs.
map
, filter
, and collect
Welcome to this lesson on leveraging common iterator functions in Rust. We'll explore map
, filter
, and collect
, building upon your existing knowledge of how iterators are created. These tools are fundamental for writing expressive, efficient, and idiomatic Rust code for data processing.
Before diving into iterator adapters, let's briefly recall the three primary methods for obtaining an iterator from a collection in Rust:
into_iter()
: This method consumes the collection it's called on. It takes ownership of the data and yields owned values of type T
. Once into_iter()
is used, the original collection can no longer be accessed.
iter()
: This method borrows the collection immutably. It yields immutable references to the items within the collection, specifically of type &T
. The original collection remains accessible after creating the iterator.
iter_mut()
: This method borrows the collection mutably. It yields mutable references to the items, of type &mut T
, allowing you to modify the elements of the collection in place. The original collection is mutably borrowed for the lifetime of the iterator.
Understanding which of these to use is crucial as it dictates the type of data your iterator will yield and, consequently, how you'll interact with it in subsequent operations.
map
and filter
Iterator adapters are methods that transform an iterator into a new iterator. They are "lazy," meaning they don't perform any work until a consuming method is called. Two of the most frequently used adapters are map
and filter
.
map
: The map
adapter transforms each element of an iterator into a new element by applying a given closure. For an iterator yielding items of type A
, map
takes a closure Fn(A) -> B
and produces a new iterator that yields items of type B
.
filter
: The filter
adapter creates a new iterator that yields only those elements for which a given closure (often called a predicate) returns true
. The closure must be of type Fn(&Item) -> bool
, where Item
is the type of element the iterator yields. The resulting iterator yields elements of the same type as the original iterator.
collect
MethodWhile adapters like map
and filter
create new iterators, they don't actually execute the iteration or produce a final result. For that, we need a consuming adapter. The most versatile consuming adapter is collect
.
collect
: This method gathers all items from an iterator and assembles them into a specified collection, such as a Vec
, HashMap
, String
, or any other type that implements the FromIterator
trait. A crucial aspect of collect
is that Rust often needs a type annotation to determine the target collection type, as collect
is generic and can produce many different kinds of collections.
Let's explore how these functions work together through practical examples.
map
and collect
with Vec<u32>
This example demonstrates how to transform elements in a vector of numbers and collect them into a new vector.
Initial Setup:
We start with a vector of unsigned 32-bit integers.
Creating an Iterator and Using map
:
We call vals.iter()
to get an iterator that yields immutable references (&u32
) to the items in vals
. Then, we use map
to increment each number. The closure |x: &u32| *x + 1
takes a reference x
of type &u32
. We dereference x
using *x
to get the u32
value and then add 1.
Note: For Copy
types like u32
, Rust's auto-dereferencing allows x + 1
to work as well, but *x + 1
is more explicit about the dereferencing operation.
Using collect
:
The collect()
method is called on the iterator returned by map
. The type annotation let v2: Vec<u32> = ...
informs collect
that we want to gather the results into a new Vec<u32>
.
If you run this (within a main
function), the output will be:
v2 [2, 3, 4, 5, 6]
A Note on Closures
Closures are anonymous functions you can store in a variable or pass as arguments to other functions.
Syntax: |input_params| body_expression
for a single expression, or |input_params| { /* multi-line body */ }
for more complex logic.
Type Inference: Rust's compiler is often able to infer the types of closure parameters and their return values from the context. However, explicit type annotations, as seen with |x: &u32|
, can improve clarity and are sometimes necessary.
Curly Braces {}
: Optional for single-expression bodies; required for multi-line bodies or when a block is needed for scope.
collect
- Vec
vs. HashMap
This example showcases how collect()
can be used to create different collection types from the same transformed iterator data.
Initial Setup:
We'll use a vector of tuples.
Transforming and Collecting into Vec<(String, u32)>
:
We iterate over vals
using iter()
, which yields &(&str, u32)
. The map
closure |v| (v.0.to_string(), v.1 + 1)
transforms each tuple:
v.0
(which is &str
) is converted to an owned String
using to_string()
.
v.1
(which is u32
) is incremented by 1.
The result is collected into a new Vec<(String, u32)>
.
Output:
vec [("a", 2), ("b", 3), ("c", 4)]
Transforming and Collecting into HashMap<String, u32>
:
Using the exact same vals.iter().map(...)
chain, we produce an iterator of (String, u32)
tuples. This time, by changing the type annotation for collect()
, we gather these key-value pairs into a HashMap<String, u32>
.
Output (order in HashMaps is not guaranteed):
hash map {"c": 4, "a": 2, "b": 3}
The key takeaway here is the power and flexibility of collect
. The same iterator transformation logic can populate different kinds of collections, abstracting away the specific insertion mechanisms (like push
for Vec
or insert
for HashMap
).
Iterator adapters can be chained together, allowing you to create sophisticated data processing pipelines in a very readable and declarative manner.
filter
then map
with iter()
This example demonstrates filtering elements before transforming them.
Initial Setup:
Chaining Operations:
vals.iter()
: Creates an iterator yielding &u32
(references).
.filter(|x: &&u32| **x <= 3)
:
The filter
adapter takes a closure. Since vals.iter()
yields &u32
(let's call this Item
), the closure for filter
receives a reference to this item, &Item
, which becomes &&u32
.
**x
performs a double dereference: the first *
dereferences &&u32
to &u32
, and the second *
dereferences &u32
to u32
for the comparison.
This filter keeps only elements whose values are less than or equal to 3.
.map(|x: &u32| *x + 1)
:
The map
closure receives x
of type &u32
. This is because filter
passes through items of the original iterator's item type (&u32
in this case) if they satisfy the predicate.
*x + 1
dereferences the &u32
to u32
and increments the value.
.collect()
: Gathers the results into a Vec<u32>
.
Output:
filter -> map [2, 3, 4]
Data flow:
Original vals
: [1, 2, 3, 4, 5]
iter()
yields: &1, &2, &3, &4, &5
filter (value <= 3)
passes: &1, &2, &3
map (value + 1)
transforms to: 2, 3, 4
collect()
creates: [2, 3, 4]
filter
then map
with into_iter()
Let's see how the types change when using into_iter()
, which moves ownership.
Initial Setup:
We'll create a new vector to demonstrate into_iter
consuming it.
Chaining Operations with into_iter()
:
vals_for_into_iter.into_iter()
: Creates an iterator yielding owned u32
values. This consumes vals_for_into_iter
.
.filter(|x: &u32| *x <= 3)
:
Since into_iter()
yields u32
(let's call this Item
), the filter
closure receives &Item
, which is &u32
.
*x
dereferences &u32
to u32
for the comparison.
.map(|x: u32| x + 1)
:
The map
closure now receives x
as an owned u32
value, because filter
passes through items of type u32
(the Item
type of the iterator from into_iter
).
No dereference is needed for x + 1
as x
is already a u32
.
.collect()
: Gathers results into a Vec<u32>
.
Output:
into_iter filter -> map [2, 3, 4]
The result is the same, but the types handled by the closures differ due to into_iter()
yielding owned values instead of references.
Iterators are Lazy: Adapters like map
and filter
don't perform their operations immediately. They construct a new iterator that represents the sequence of operations. The actual work is only executed when a consuming method like collect()
is called. This laziness can lead to performance optimizations, as unnecessary intermediate collections might be avoided.
Power of Chaining: Iterator adapters can be elegantly chained together, creating expressive and concise data processing pipelines. This often leads to more readable code compared to manual loops with conditional logic.
Role of Rust's Type System: The strong type system in Rust, combined with type inference, plays a vital role. While types are often inferred, explicit type annotations (especially for the return type of collect()
, and sometimes for closure parameters) are crucial for clarity and guiding the compiler.
Ownership and Borrowing Impact: Your choice between iter()
, iter_mut()
, and into_iter()
directly influences whether your closures operate on references (&T
, &mut T
) or owned values (T
). This, in turn, affects how you access and manipulate data within those closures (e.g., needing to dereference references).
Rust's iterators, along with adapters like map
and filter
, and consumers like collect
, provide a powerful, efficient, and idiomatic way to work with collections. By understanding how these components interact, especially concerning types, ownership, and laziness, you can write highly declarative and effective Rust code for a wide range of data manipulation tasks. This functional approach often leads to cleaner, more maintainable, and less error-prone programs.
A hands-on tour of Mastering Iterators in Rust: `map`, `filter`, and `collect` - Learn to transform data using `map`, refine selections with `filter`, and aggregate results into diverse collections via `collect`. Master the art of chaining these iterator methods for clean, efficient, and powerful data manipulation in Rust.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Introduction to the Rust programming language
Rust variables and functions
Scalar types, arrays, strings, enum, structs, vectors, and hash maps in Rust
Rust control flows: If / else, if let and let else, loop, match
Rust ownership, including borrow and references
Rust error handling
Rust Modules
Rust Traits
Last updated on July 11, 2025
Rust Developer
Rust Programming BasicsDuration: 6min
Duration: 18min
Duration: 47min
Duration: 15min
Duration: 19min
Duration: 8min
Duration: 12min
Duration: 46min
Duration: 14min
Course Overview
About the course
Introduction to the Rust programming language
Rust variables and functions
Scalar types, arrays, strings, enum, structs, vectors, and hash maps in Rust
Rust control flows: If / else, if let and let else, loop, match
Rust ownership, including borrow and references
Rust error handling
Rust Modules
Rust Traits
Last updated on July 11, 2025