Closures and iterators
Capture values from the environment with closures, then chain iterator adapters for expressive, zero-cost data transformations.
- Write closures that capture values by reference or by move
- Distinguish Fn, FnMut, and FnOnce based on how the closure uses captured values
- Chain iterator adapters including map, filter, flat_map, and collect
- Implement the Iterator trait for a custom type
Closures are anonymous functions that can capture values from their surrounding scope. Iterators are lazy sequences that produce values on demand. Together they form Rust's functional programming layer — expressive chains of transformations that compile down to tight loops with no overhead.
Closures
A closure is written with |params| body:
let add_one = |x: i32| x + 1;
println!("{}", add_one(5)); // 6
let greet = |name| format!("Hello, {}!", name);
println!("{}", greet("Ferris"));Type annotations are optional — the compiler infers them from usage. Unlike functions, closures can capture variables from the enclosing scope:
let offset = 10;
let add_offset = |x| x + offset; // captures `offset` by reference
println!("{}", add_offset(5)); // 15Capture modes and Fn traits
How a closure captures its environment determines which Fn trait it implements:
| Trait | What the closure can do with captured values |
|---|---|
Fn | Borrows captured values (can call many times) |
FnMut | Mutably borrows (can call many times, but not concurrently) |
FnOnce | Takes ownership (can only call once) |
Every closure implements at least FnOnce. If it doesn't consume or mutate captured values, it also implements Fn.
let s = String::from("hello");
// FnOnce — consumes s
let consume = || drop(s);
consume(); // fine
// consume(); // ERROR — s already moved into the first call
// FnMut — mutates captured counter
let mut count = 0;
let mut increment = || { count += 1; count };
println!("{}", increment()); // 1
println!("{}", increment()); // 2move closures
Add move to force the closure to take ownership of everything it captures — required when the closure outlives the scope it was defined in (e.g., when passing it to a thread):
let name = String::from("Ada");
let greeting = move || format!("Hello, {}!", name);
// name is no longer accessible here — it was moved into the closure
println!("{}", greeting());move closures are essential for threads. When you spawn a thread, the closure must own its data (or have 'static references) because the current scope may end before the thread does. The compiler will tell you when move is needed.
The Iterator trait
The Iterator trait requires one method — next() — that returns Option<Self::Item>:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ... dozens of default methods
}When next() returns Some(item), the iterator yields that item. When it returns None, the sequence is exhausted. All the adapter methods (map, filter, etc.) are default implementations built on top of next().
Iterator adapters
Adapters transform an iterator into another iterator. They are lazy — nothing is computed until you consume the iterator:
let v = vec![1, 2, 3, 4, 5];
// map — transform each element
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
// filter — keep only elements matching a predicate
let evens: Vec<&i32> = v.iter().filter(|x| **x % 2 == 0).collect();
// chain of adapters
let result: Vec<i32> = v.iter()
.filter(|&&x| x % 2 != 0) // odd numbers
.map(|&x| x * x) // square them
.collect();
// result = [1, 9, 25]The double-dereference **x in filter above appears because iter() yields &i32 references, and the closure parameter is then &&i32. Using into_iter() yields owned values; iter_mut() yields &mut i32. The right choice depends on whether you want ownership, shared access, or mutable access.
flat_map
flat_map applies a closure that returns an iterator and flattens the results:
let words = vec!["hello world", "foo bar"];
let letters: Vec<&str> = words.iter()
.flat_map(|s| s.split_whitespace())
.collect();
// ["hello", "world", "foo", "bar"]collect and type inference
collect() consumes the iterator and assembles the results into a collection. The type of collection is usually inferred from context:
let squares: Vec<i32> = (1..=5).map(|x| x * x).collect();
let unique: std::collections::HashSet<i32> = vec![1, 2, 2, 3].into_iter().collect();If the compiler can't infer what to collect into, annotate the variable or use the turbofish: .collect::<Vec<_>>().
Implementing Iterator for a custom type
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self { Counter { count: 0, max } }
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
// Counter now gets all iterator adapters for free:
let sum: u32 = Counter::new(5).filter(|x| x % 2 != 0).sum();
// sum = 1 + 3 + 5 = 9Check your understanding
Knowledge check
- 1.A closure that reads a captured String value (without consuming or mutating it) implements which Fn trait?
- 2.Iterator adapters like map and filter do their work as soon as they are called.
- 3.What does flat_map do differently from map?
Do it yourself
Use iterator adapters to solve these without explicit loops:
- Given
words: Vec<String>, produce aVec<String>containing only words longer than 4 characters, uppercased. - Given
nums: Vec<i32>, compute the sum of squares of all odd numbers. - Implement
Iteratorfor a Fibonacci sequence type that yields the next Fibonacci number on each call.
Where to go next
Closures and iterators complete the intermediate track's core toolkit. The lab at the end of this module has scenario questions on all the topics covered. After that, the advanced track opens with concurrency — where closures, ownership, and Send/Sync combine to make data-race-free parallelism a compile-time guarantee.