A closure is an anonymous function you can store in a variable or pass to another function, and which can use variables from the place where it was written. An iterator is a value that produces a sequence of items, one at a time, on request. In Rust the two go together: most iterator methods, such as map, filter and fold, take a closure that says what to do with each item. Together they let you replace many hand-written loops with short, readable pipelines that compile to code as fast as the loops they replace.
If you know Python's lambda and generator expressions, JavaScript's arrow functions and array.map, or Java's lambdas and streams, the ideas will feel familiar. What is new in Rust is ownership: a closure has to decide whether it borrows the variables it uses, borrows them mutably, or takes them over. That decision determines which of three traits, Fn, FnMut or FnOnce, the closure implements, and it explains most of the compiler errors you will meet. This lesson covers closure syntax and type inference, the three capture modes, the three closure traits, move closures and returning closures, the Iterator trait and next, the common adapters, laziness, collect, the performance of iterators compared with loops, and implementing Iterator for your own types. Interviewers commonly ask about the differences between Fn, FnMut and FnOnce, what move does, and why iterators are lazy.
Closure syntax
A closure is written with its parameters between vertical bars, followed by an expression:
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
let add_one_closure = |x: i32| -> i32 { x + 1 };
let add_one_short = |x| x + 1;
let greet = || println!("no parameters");
println!("{} {} {}", add_one(1), add_one_closure(2), add_one_short(3));
greet();
let gst_percent = 18;
let with_tax = |price: u32| price + price * gst_percent / 100;
println!("500 with tax = {}", with_tax(500));
println!("1200 with tax = {}", with_tax(1200));
let mut scores = vec![("Meena", 72), ("Arun", 91), ("Zoya", 85)];
scores.sort_by_key(|&(_, score)| std::cmp::Reverse(score));
println!("{scores:?}");
}
Output:
2 3 4
no parameters
500 with tax = 590
1200 with tax = 1416
[("Arun", 91), ("Zoya", 85), ("Meena", 72)]
The three closures near the top are increasingly short forms of the same idea:
| Form | Meaning |
|---|---|
|x: i32| -> i32 { x + 1 } | Fully annotated, like a function |
|x| x + 1 | Types inferred; a single expression needs no braces |
|| println!("...") | No parameters |
Unlike functions, closures do not need type annotations. A function is part of an interface that other code relies on, so Rust insists its types are written down. A closure is usually local and short-lived, so the compiler infers its parameter and return types from how it is used.
The interesting line is with_tax. It uses gst_percent, a variable defined outside the closure. A function cannot do that; a closure captures variables from its surrounding scope, its environment. 500 + 500 × 18 / 100 = 590, and 1,200 + 216 = 1,416.
The last example passes a closure to sort_by_key, the most common way you will use closures: as an argument that customises a library function. |&(_, score)| std::cmp::Reverse(score) destructures each tuple and returns its score wrapped in Reverse, which flips the ordering so the highest score comes first.
Reading the compiler error: E0308, a closure has one type per parameter
Inference picks one concrete type for each closure parameter, the first time the closure is used. A closure is not generic. This does not compile:
fn main() {
let identity = |x| x;
let s = identity(String::from("hello"));
let n = identity(5);
println!("{s} {n}");
}
error[E0308]: mismatched types
--> infer_err.rs:4:22
|
4 | let n = identity(5);
| -------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> infer_err.rs:3:22
|
3 | let s = identity(String::from("hello"));
| -------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> infer_err.rs:2:21
|
2 | let identity = |x| x;
| ^
help: try using a conversion method
|
4 | let n = identity(5.to_string());
| ++++++++++++
The notes explain the reasoning: the first call fixed x to String, so the second call with an integer is a type mismatch. If you need the same logic for several types, write a generic function fn identity<T>(x: T) -> T (lesson 11).
Capturing the environment
A closure captures each variable it uses in the least demanding way that its body allows. There are three ways, mirroring the three ways of passing a value to a function:
fn main() {
// 1. Borrow immutably: the closure only reads `name`.
let name = String::from("Kavya");
let say_hi = || println!("hi, {name}");
say_hi();
say_hi();
println!("name is still usable: {name}");
// 2. Borrow mutably: the closure changes `count`, so it must be `mut`.
let mut count = 0;
let mut bump = || count += 1;
bump();
bump();
bump();
println!("count = {count}");
// 3. Take ownership: the closure drops `log`, so it can run only once.
let log = vec![String::from("boot"), String::from("ready")];
let consume = || {
let owned = log;
println!("consumed {} entries", owned.len());
};
consume();
}
Output:
hi, Kavya
hi, Kavya
name is still usable: Kavya
count = 3
consumed 2 entries
- By shared reference.
say_hionly readsname, so it captures&name. You can call it many times, andnamestays usable, both while the closure exists and after. - By mutable reference.
bumpchangescount, so it captures&mut count. Calling it changes the closure's captured state, so the closure variable itself must be declaredlet mut bump. Whilebumpis alive and will be used again, nothing else may touchcount. - By value. The body of
consumemoveslogintoowned, so the closure must ownlog: it captures it by value. After the first call,loghas been moved out of the closure and dropped, soconsumecan be called only once.
A closure is compiled to an anonymous struct that holds exactly what it captured. You can see that with size_of_val:
use std::mem::size_of_val;
fn main() {
let rate = 18_u64;
let label = String::from("GST");
let no_capture = |x: u64| x * 2;
let by_ref = |x: u64| x * rate / 100;
let by_move = move |x: u64| format!("{label}: {}", x * rate / 100);
println!("{} {} {}", no_capture(10), by_ref(500), by_move(500));
println!(
"sizes: no capture {}, by reference {}, by move {}",
size_of_val(&no_capture),
size_of_val(&by_ref),
size_of_val(&by_move)
);
let as_fn_pointer: fn(u64) -> u64 = no_capture;
println!("through a fn pointer: {}", as_fn_pointer(21));
}
Output:
20 90 GST: 90
sizes: no capture 0, by reference 8, by move 32
through a fn pointer: 42
no_capture struct { } 0 bytes
by_ref struct { rate: &u64 } 8 bytes
by_move struct { label: String, rate: u64 } 24 + 8 = 32 bytes
A closure that captures nothing takes no space and can be converted to a plain function pointer type, fn(u64) -> u64. Every closure has its own unique type that you cannot write in source code, which is why you pass closures with generics or impl Fn rather than naming their type. There is no hidden heap allocation and no garbage collector involved: captured data lives inside the closure value, exactly where you put it.
Since the 2021 edition, a closure captures only the fields it uses: a closure that reads user.name captures &user.name, not the whole user, so another part of the code can still change user.age.
Reading the compiler error: E0502, using a variable a closure has borrowed mutably
The capture is a real borrow, with all the usual rules. This does not compile:
fn main() {
let mut count = 0;
let mut bump = || count += 1;
bump();
println!("count = {count}");
bump();
}
error[E0502]: cannot borrow `count` as immutable because it is also borrowed as mutable
--> capture_err.rs:5:24
|
3 | let mut bump = || count += 1;
| -- ----- first borrow occurs due to use of `count` in closure
| |
| mutable borrow occurs here
4 | bump();
5 | println!("count = {count}");
| ^^^^^ immutable borrow occurs here
6 | bump();
| ---- mutable borrow later used here
The message reads like an ordinary E0502 from lesson 5, and that is exactly what it is. Creating bump borrows count mutably, and the borrow lasts until the last use of bump on line 6. Reading count on line 5 falls inside that borrow. Remove the second bump() call, or move the println! after it, and the program compiles: the borrow ends after the last call.
Fn, FnMut and FnOnce
The way a closure uses its captures determines which closure traits it implements. These are the traits you write in bounds when a function accepts a closure.
| Trait | Can be called | The closure may | Typical example |
|---|---|---|---|
FnOnce | At least once | Move captured values out (consume them) | Option::unwrap_or_else, thread::spawn |
FnMut | Many times, needs &mut access | Change captured values | Iterator::for_each, sort_by_key, retain |
Fn | Many times, even through a shared reference | Only read captured values | Stored callbacks such as Box<dyn Fn(Event)> handlers |
The traits form a hierarchy: every Fn closure is also FnMut, and every FnMut closure is also FnOnce.
FnOnce "callable once" (every closure)
^
FnMut "callable repeatedly, may mutate"
^
Fn "callable repeatedly through a shared reference"
A function that accepts a closure should ask for the least it needs, which is the widest trait. If it calls the closure once, ask for FnOnce: then callers may pass any closure at all. If it calls it repeatedly and does not need shared access, ask for FnMut. Ask for Fn only if you call it through a shared reference, for example from several places at once.
fn call_twice<F: Fn() -> String>(f: F) -> String {
format!("{} / {}", f(), f())
}
fn repeat_mut<F: FnMut()>(times: usize, mut f: F) {
for _ in 0..times {
f();
}
}
fn run_once<F: FnOnce() -> Vec<String>>(f: F) -> usize {
f().len()
}
fn main() {
let city = String::from("Indore");
println!("{}", call_twice(|| city.to_uppercase()));
let mut visits = Vec::new();
let mut next_id = 0;
repeat_mut(3, || {
next_id += 10;
visits.push(next_id);
});
println!("{visits:?}");
let names = vec![String::from("a"), String::from("b")];
println!("{}", run_once(move || names));
println!("{}", run_once(|| vec![city.clone()]));
repeat_mut(2, || println!("Fn closures are also FnMut"));
}
Output:
INDORE / INDORE
[10, 20, 30]
2
1
Fn closures are also FnMut
Fn closures are also FnMut
call_twicerequiresFn; the closure|| city.to_uppercase()only readscity, so it qualifies.repeat_mutrequiresFnMutand declares its parametermut f, because calling anFnMutclosure needs mutable access to it. The closure mutatesnext_idandvisits.run_oncerequiresFnOnce;move || namesreturns its captured vector, consuming it.- The last two calls show the hierarchy: a closure that only reads (
Fn) is accepted whereFnOnceorFnMutis required.
Ordinary functions implement all three traits too, so you can pass a function name wherever a closure is expected, as in map(str::len) or filter_map(parse_user).
Reading the compiler error: E0507, moving out of a captured variable in an FnMut closure
This does not compile:
fn repeat_mut<F: FnMut()>(times: usize, mut f: F) {
for _ in 0..times {
f();
}
}
fn main() {
let report = String::from("quarterly report");
repeat_mut(2, || {
let sent = report;
println!("sending {sent}");
});
}
error[E0507]: cannot move out of `report`, a captured variable in an `FnMut` closure
--> fnonce_err.rs:10:20
|
8 | let report = String::from("quarterly report");
| ------ captured outer variable
9 | repeat_mut(2, || {
| -- captured by this `FnMut` closure
10 | let sent = report;
| ^^^^^^ move occurs because `report` has type `String`, which does not implement the `Copy` trait
|
help: `Fn` and `FnMut` closures require captured values to be able to be consumed multiple times, but `FnOnce` closures may consume them only once
--> fnonce_err.rs:1:18
|
1 | fn repeat_mut<F: FnMut()>(times: usize, mut f: F) {
| ^^^^^^^
help: consider borrowing here
|
10 | let sent = &report;
| +
help: consider cloning the value if the performance cost is acceptable
|
10 | let sent = report.clone();
| ++++++++
repeat_mut will call the closure twice. The closure's body moves report out, which can happen only once: the second call would find nothing there. The first help line states the rule precisely: Fn and FnMut closures must be able to run again, so they cannot consume their captures. The fixes follow from what you mean:
- If each call should only use the report, borrow it:
let sent = &report;, or justprintln!("sending {report}"). - If each call needs its own owned copy,
report.clone(). - If it really should happen once, call a function that takes
FnOnceinstead.
Interview tip
A crisp answer to "explain Fn, FnMut and FnOnce": "They describe how a closure uses what it captured. Fn only reads, so it can be called any number of times, even concurrently. FnMut changes its captures, so each call needs exclusive access. FnOnce may consume its captures, so it is guaranteed to be callable only once. Every closure is FnOnce; Fn is a subtrait of FnMut, which is a subtrait of FnOnce. A function should accept the most general bound it can, which means FnOnce if it calls once."
move closures
The move keyword before a closure forces it to capture every variable by value, even if the body only reads it. It changes how variables are captured, not which trait the closure implements: a move closure that only reads its captures is still Fn.
You need move when the closure must outlive the scope that created it: when you return it from a function, store it in a struct, or send it to another thread.
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn make_greeter(greeting: &str) -> Box<dyn Fn(&str) -> String> {
let greeting = greeting.to_string();
Box::new(move |name| format!("{greeting}, {name}!"))
}
fn main() {
let add5 = make_adder(5);
let add10 = make_adder(10);
println!("{} {}", add5(1), add10(1));
let greeters = [make_greeter("Hello"), make_greeter("Vanakkam")];
for g in &greeters {
println!("{}", g("Ishaan"));
}
let data: Vec<i32> = (1..=3).collect();
let handle = std::thread::spawn(move || data.iter().sum::<i32>());
println!("sum from thread = {}", handle.join().unwrap());
}
Output:
6 11
Hello, Ishaan!
Vanakkam, Ishaan!
sum from thread = 6
make_adderreturnsimpl Fn(i32) -> i32: one concrete, unnamed closure type. Each call creates a new closure that owns its own copy ofn, soadd5andadd10are independent.make_greeterreturnsBox<dyn Fn(&str) -> String>, a trait object (lesson 12). Boxing lets closures with different captured data be stored together in one array.thread::spawnrequires its closure to be'static(lesson 13), because the new thread may outlivemain's local variables.movetransfers ownership ofdatainto the closure, and so into the thread. Lesson 16 covers threads properly.
Reading the compiler error: E0373, closure may outlive the current function
Without move, returning the closure does not compile:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
|x| x + n
}
fn main() {
println!("{}", make_adder(5)(1));
}
error[E0373]: closure may outlive the current function, but it borrows `n`, which is owned by the current function
--> move_err.rs:2:5
|
2 | |x| x + n
| ^^^ - `n` is borrowed here
| |
| may outlive borrowed value `n`
|
note: closure is returned here
--> move_err.rs:2:5
|
2 | |x| x + n
| ^^^^^^^^^
help: to force the closure to take ownership of `n` (and any other referenced variables), use the `move` keyword
|
2 | move |x| x + n
| ++++
The body only reads n, so by default the closure captures &n. But n is a parameter of make_adder and is dropped when the function returns, while the closure is returned and lives on: a dangling reference in the making. The help line is the fix: move makes the closure own its copy of n. Because i32 is Copy, moving it copies the number; with a String, the original variable could no longer be used after the closure is created, so clone it first if you still need it.
Pitfall: move does not make a closure FnOnce
move decides how captures are stored (owned instead of borrowed). What the body does with them decides the trait. move |x| x + n only reads n, so it is Fn and can be called any number of times.
The Iterator trait
An iterator is any type that implements the Iterator trait. Its core is one associated type and one method:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ...plus dozens of provided methods: map, filter, sum, collect, ...
}
next returns Some(item) while there are items and None when the sequence is finished. Everything else is built on top of it as default methods, which is why implementing next for your own type (as lesson 12 did for Countdown) gives you the whole toolkit.
fn main() {
let v = [10, 20, 30];
let mut it = v.iter();
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
let mut names = vec![String::from("asha"), String::from("ravi")];
for name in names.iter() {
print!("{name} ");
}
println!();
for name in names.iter_mut() {
name.make_ascii_uppercase();
}
println!("{names:?}");
for name in names.into_iter() {
print!("owned {name}; ");
}
println!();
}
Output:
Some(10)
Some(20)
Some(30)
None
asha ravi
["ASHA", "RAVI"]
owned ASHA; owned RAVI;
it must be mut, because next changes the iterator's position. Once it returns None, the iterator is exhausted.
Collections offer three iterators, matching the three ways of accessing data:
| Method | Yields | The collection afterwards | for loop shorthand |
|---|---|---|---|
iter() | &T, shared references | Unchanged, still usable | for x in &v |
iter_mut() | &mut T, mutable references | Possibly modified, still usable | for x in &mut v |
into_iter() | T, owned values | Consumed, moved into the loop | for x in v |
A for loop is sugar over this trait. for x in v { ... } calls IntoIterator::into_iter(v) and then calls next until it returns None:
for x in v { body } let mut it = IntoIterator::into_iter(v);
==> while let Some(x) = it.next() { body }
IntoIterator is the trait for "something that can be turned into an iterator". Vec<T>, &Vec<T> and &mut Vec<T> each implement it, giving the three rows of the table.
Iterator adapters and consumers
Iterator methods come in two kinds:
- Adapters take an iterator and return a new iterator that transforms the items:
map,filter,enumerate,zip,take,skip,flat_map,chain,rev,step_by,take_while,inspect. - Consumers (also called terminal operations) pull items through the pipeline and produce a final value:
collect,sum,count,fold,max,min,any,all,find,position,for_each,last.
fn main() {
let marks = [67, 92, 45, 88, 73, 39, 95];
let doubled: Vec<i32> = marks.iter().map(|m| m * 2).collect();
let passed: Vec<&i32> = marks.iter().filter(|&&m| m >= 50).collect();
println!("map: {doubled:?}");
println!("filter: {passed:?}");
for (rank, m) in marks.iter().enumerate().take(3) {
println!("enumerate+take: #{rank} = {m}");
}
let names = ["Asha", "Bilal", "Chitra"];
let ages = [21, 34, 28];
let pairs: Vec<String> = names
.iter()
.zip(ages.iter())
.map(|(n, a)| format!("{n}:{a}"))
.collect();
println!("zip: {pairs:?}");
let skipped: Vec<i32> = marks.iter().skip(5).copied().collect();
println!("skip: {skipped:?}");
let sentences = ["the quick fox", "jumps over", "the dog"];
let words: Vec<&str> = sentences.iter().flat_map(|s| s.split(' ')).collect();
println!("flat_map: {words:?}");
let grades = marks.iter().fold(String::new(), |mut acc, &m| {
acc.push(if m >= 50 { 'P' } else { 'F' });
acc
});
let longest = names
.iter()
.fold("", |best, n| if n.len() > best.len() { n } else { best });
println!("fold: grades {grades}, longest name {longest}");
let sum: i32 = marks.iter().sum();
let max = marks.iter().max();
let any_fail = marks.iter().any(|&m| m < 40);
let all_pos = marks.iter().all(|&m| m > 0);
let first_above_90 = marks.iter().position(|&m| m > 90);
println!(
"sum {sum}, max {max:?}, any fail {any_fail}, all positive {all_pos}, first > 90 at {first_above_90:?}"
);
}
Output:
map: [134, 184, 90, 176, 146, 78, 190]
filter: [67, 92, 88, 73, 95]
enumerate+take: #0 = 67
enumerate+take: #1 = 92
enumerate+take: #2 = 45
zip: ["Asha:21", "Bilal:34", "Chitra:28"]
skip: [39, 95]
flat_map: ["the", "quick", "fox", "jumps", "over", "the", "dog"]
fold: grades PPFPPFP, longest name Chitra
sum 499, max Some(95), any fail true, all positive true, first > 90 at Some(1)
What each one did:
| Method | What it does | In the example |
|---|---|---|
map(f) | Applies f to every item | Doubles every mark |
filter(p) | Keeps items where p returns true | Keeps the five marks of 50 or more |
enumerate() | Pairs each item with its index, from 0 | (0, 67), (1, 92), ... |
take(n) | Stops after n items | First three only |
zip(other) | Pairs items from two iterators; stops at the shorter | ("Asha", 21), ... |
skip(n) | Discards the first n items | Leaves the last two marks |
flat_map(f) | Maps each item to an iterator and flattens the results | Three sentences into seven words |
fold(init, f) | Carries an accumulator through every item | Builds the grade string PPFPPFP |
sum, max, any, all, position | Common consumers | 499, 95, a fail exists, all positive, index 1 |
The sum checks out: 67 + 92 + 45 + 88 + 73 + 39 + 95 = 499. Two details need explanation:
filter(|&&m| m >= 50):marks.iter()yields&i32, andfilterpasses its closure a reference to each item, so the parameter is&&i32. The pattern&&mstrips both references. You will see|x| **x >= 50or|&&x|often; both mean the same..copied()turns an iterator of&i32into one ofi32, forCopytypes..cloned()does the same withclonefor other types.
fold is the general-purpose consumer: sum, count and max can all be written with it. Clippy suggests the specific method when one exists, because sum() says what you mean.
Laziness
Adapters do nothing on their own. v.iter().map(f) builds a Map value that holds the original iterator and the closure, and calls neither. Work happens only when a consumer calls next. This is called laziness. The compiler warns when you forget the consumer. This compiles with a warning:
fn main() {
let v = vec![1, 2, 3];
v.iter().map(|x| {
println!("processing {x}");
x * 2
});
println!("done");
}
warning: unused `Map` that must be used
--> lazy.rs:3:5
|
3 | / v.iter().map(|x| {
4 | | println!("processing {x}");
5 | | x * 2
6 | | });
| |______^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
|
3 | let _ = v.iter().map(|x| {
| +++++++
Output:
done
"processing" is never printed. The note says it plainly: iterators are lazy and do nothing unless consumed. If you want to run a closure for its side effects, use a for loop or for_each.
Laziness is not just a trap; it makes pipelines efficient. Items flow through the whole chain one at a time, and the pipeline stops pulling as soon as the consumer has what it needs:
fn main() {
let result: Vec<i32> = (1..=10)
.inspect(|x| println!(" source yields {x}"))
.map(|x| x * x)
.filter(|sq| sq % 2 == 1)
.take(2)
.collect();
println!("{result:?}");
let first_big = (1..).map(|n: u64| n * n * n).find(|&c| c > 1000);
println!("first cube above 1000: {first_big:?}");
}
Output:
source yields 1
source yields 2
source yields 3
[1, 9]
first cube above 1000: Some(1331)
Trace of the first pipeline:
| Source item | map (square) | filter (odd?) | take(2) has |
|---|---|---|---|
| 1 | 1 | kept | [1] |
| 2 | 4 | dropped | [1] |
| 3 | 9 | kept | [1, 9], done |
| 4 to 10 | never produced |
Only three of the ten numbers were ever generated, which inspect (an adapter that lets you look at each item as it passes) proves. No intermediate vectors were built between the stages, unlike Python's list comprehensions or JavaScript's array.map().filter(), which create a full array at each step.
The second pipeline goes further: (1..) is an infinite range. It works because find stops at the first match: 11³ = 1,331 is the first cube above 1,000 (10³ is exactly 1,000, which is not greater).
eager (JavaScript arrays): lazy (Rust iterators):
[1..10] -> map -> [10 items] 1 -> map -> filter -> take
-> filter -> [5 items] 2 -> map -> filter (dropped)
-> slice -> [2 items] 3 -> map -> filter -> take -> done
collect and its many targets
collect turns an iterator into a collection. It is generic over its return type (via the FromIterator trait), so you choose the collection with an annotation or a turbofish, exactly as lesson 11 described.
use std::collections::{BTreeMap, HashSet};
fn main() {
let text = "to be or not to be";
let words: Vec<&str> = text.split(' ').collect();
let unique: HashSet<&str> = text.split(' ').collect();
let caps: String = text
.split(' ')
.map(|w| w.to_uppercase())
.collect::<Vec<_>>()
.join("-");
let initials: String = text.split(' ').filter_map(|w| w.chars().next()).collect();
println!(
"{} words, {} unique, {caps}, {initials}",
words.len(),
unique.len()
);
let counts: BTreeMap<&str, usize> = text.split(' ').fold(BTreeMap::new(), |mut map, w| {
*map.entry(w).or_insert(0) += 1;
map
});
println!("{counts:?}");
let good: Result<Vec<i32>, _> = ["4", "8", "15"].iter().map(|s| s.parse::<i32>()).collect();
let bad: Result<Vec<i32>, _> = ["4", "x", "15"].iter().map(|s| s.parse::<i32>()).collect();
println!("{good:?}");
println!("{bad:?}");
let (even, odd): (Vec<i32>, Vec<i32>) = (1..=9).partition(|n| n % 2 == 0);
println!("even {even:?}, odd {odd:?}");
}
Output:
6 words, 4 unique, TO-BE-OR-NOT-TO-BE, tbontb
{"be": 2, "not": 1, "or": 1, "to": 2}
Ok([4, 8, 15])
Err(ParseIntError { kind: InvalidDigit })
even [2, 4, 6, 8], odd [1, 3, 5, 7, 9]
- The same
splititerator can become aVec, aHashSet(4 unique words: to, be, or, not) or, from an iterator ofchars, aString. filter_mapcombinesmapandfilter: it keeps only theSomevalues. Here it takes each word's first character.foldwith aBTreeMapcounts words; aBTreeMapprints its keys in sorted order.- Collecting an iterator of
Results intoResult<Vec<_>, _>givesOkwith all values, or the first error, and stops at that error. Lesson 9 introduced this pattern. partitionsplits one iterator into two collections by a predicate.
Iterators versus loops: performance
A common question is whether this style is slower than a hand-written loop. In optimised (--release) builds, iterator chains compile to essentially the same machine code as the equivalent loop, and sometimes better code. Each adapter is a small struct, the closures are monomorphised and inlined, and the optimiser sees through the layers. This is another case of the zero-cost abstraction principle from lesson 11.
fn sum_of_squares_loop(values: &[i64]) -> i64 {
let mut total = 0;
let mut i = 0;
while i < values.len() {
if values[i] % 2 == 0 {
total += values[i] * values[i];
}
i += 1;
}
total
}
fn sum_of_squares_iter(values: &[i64]) -> i64 {
values.iter().filter(|&&v| v % 2 == 0).map(|v| v * v).sum()
}
fn main() {
let data: Vec<i64> = (1..=1_000).collect();
println!("loop: {}", sum_of_squares_loop(&data));
println!("iter: {}", sum_of_squares_iter(&data));
}
Output:
loop: 167167000
iter: 167167000
Both functions sum the squares of the even numbers from 1 to 1,000: 2² + 4² + ... + 1000² = 4 × (1² + ... + 500²) = 4 × 41,791,750 = 167,167,000. The indexed version performs values[i], which is bounds-checked: the program must confirm i is in range or panic. The compiler can often prove the check redundant and remove it, but not always. The iterator version has no indices, so there is nothing to check. That is why idiomatic Rust prefers iterators over index loops, for clarity and, when it matters, for speed.
Interview tip
If asked "are iterators slower than loops in Rust?", say no for release builds: adapters are generic structs whose closures are inlined, so a chain like filter().map().sum() compiles to a single loop, and avoiding indexing can remove bounds checks. Add the caveat: in debug builds iterator-heavy code can be noticeably slower because nothing is inlined, so always measure performance with --release.
Implementing Iterator for your own types
Implementing Iterator means choosing the Item type and writing next. You can also make your collection work in for loops by implementing IntoIterator.
struct Collatz {
current: Option<u64>,
}
impl Iterator for Collatz {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let n = self.current?;
self.current = match n {
1 => None,
even if even % 2 == 0 => Some(even / 2),
odd => Some(3 * odd + 1),
};
Some(n)
}
}
fn collatz(start: u64) -> Collatz {
Collatz {
current: Some(start),
}
}
struct Playlist {
songs: Vec<String>,
}
impl<'a> IntoIterator for &'a Playlist {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.songs.iter()
}
}
fn main() {
let steps: Vec<u64> = collatz(6).collect();
println!("{steps:?}");
let (best_start, longest) = (1..=20)
.map(|n| (n, collatz(n).count()))
.max_by_key(|&(_, len)| len)
.unwrap();
println!("start {best_start} gives the longest sequence below 21: {longest} numbers");
let peak = collatz(27).max().unwrap();
println!("27 climbs as high as {peak}");
let list = Playlist {
songs: vec![String::from("Kesariya"), String::from("Naatu Naatu")],
};
for song in &list {
println!("playing {song}");
}
}
Output:
[6, 3, 10, 5, 16, 8, 4, 2, 1]
start 19 gives the longest sequence below 21: 21 numbers
27 climbs as high as 9232
playing Kesariya
playing Naatu Naatu
Collatzyields the Collatz sequence: halve an even number, replace an odd numbernwith 3n + 1, and stop at 1. Starting at 6 gives 6, 3, 10, 5, 16, 8, 4, 2, 1. StoringOption<u64>lets the iterator still yield the final 1 and then end.- Because
Collatzis an iterator,count,maxandcollectall work on it.max_by_keyfinds the starting number up to 20 with the longest sequence. Both 18 and 19 give 21 numbers;max_by_keyreturns the last maximum it sees, so it reports 19. (min_by_keyreturns the first minimum.) - Starting at 27 the sequence climbs to 9,232 before falling back to 1, a famous example of how erratic these sequences are.
impl<'a> IntoIterator for &'a Playlistletsfor song in &listwork. ItsIntoItertype is the slice iteratorstd::slice::Iter<'a, String>, so it simply hands out the inner vector's iterator. The lifetime'a(lesson 13) ties each&Stringto the borrowed playlist.
Worked example: analysing a request log
Let us combine closures and iterators on a realistic task: parse a web server log, skip corrupt lines, and report the error count, the paths that received POST requests, the average latency per path (slowest first) and the slowest single request.
use std::collections::HashMap;
#[derive(Debug)]
struct Request<'a> {
method: &'a str,
path: &'a str,
status: u16,
millis: u32,
}
fn parse(line: &str) -> Option<Request<'_>> {
let mut parts = line.split_whitespace();
let method = parts.next()?;
let path = parts.next()?;
let status = parts.next()?.parse().ok()?;
let millis = parts.next()?.strip_suffix("ms")?.parse().ok()?;
Some(Request {
method,
path,
status,
millis,
})
}
fn main() {
let log = "\
GET /home 200 35ms
GET /search 200 180ms
POST /login 401 22ms
GET /search 500 950ms
this line is corrupt
GET /home 200 41ms
POST /cart 201 120ms
GET /search 200 210ms";
let requests: Vec<Request> = log.lines().filter_map(parse).collect();
println!("parsed {} of {} lines", requests.len(), log.lines().count());
let errors = requests.iter().filter(|r| r.status >= 400).count();
println!("errors: {errors}");
let posts: Vec<&str> = requests
.iter()
.filter(|r| r.method == "POST")
.map(|r| r.path)
.collect();
println!("POST paths: {posts:?}");
let mut by_path: HashMap<&str, Vec<u32>> = HashMap::new();
for r in &requests {
by_path.entry(r.path).or_default().push(r.millis);
}
let mut averages: Vec<(&str, u32)> = by_path
.iter()
.map(|(path, times)| (*path, times.iter().sum::<u32>() / times.len() as u32))
.collect();
averages.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (path, avg) in averages.iter().take(3) {
println!("{path:<8} avg {avg:>4} ms");
}
let slowest = requests
.iter()
.max_by_key(|r| r.millis)
.map(|r| (r.path, r.millis));
println!("slowest: {slowest:?}");
}
Output:
parsed 7 of 8 lines
errors: 2
POST paths: ["/login", "/cart"]
/search avg 446 ms
/cart avg 120 ms
/home avg 38 ms
slowest: Some(("/search", 950))
How it works, step by step:
parsereturnsOption<Request>, using?on every step that can fail.strip_suffix("ms")removes the unit;.parse().ok()?converts a parse error intoNone.Request<'_>borrows the method and path from the line, so no strings are copied (lesson 13).log.lines().filter_map(parse).collect()parses every line and silently drops the corrupt one: 7 of 8 lines survive. Passing the function nameparseworks because functions implement the closure traits.filter(...).count()counts statuses of 400 or more: the 401 and the 500, so 2.filterthenmapextracts the paths of POST requests.- The per-path grouping uses a plain
forloop with the entry API, because it mutates a map. Iterators are not always clearer; use a loop when you are building up state. - Averages use integer division.
/searchhad 180, 950 and 210 ms: 1,340 / 3 = 446 (rounded down)./carthas one request of 120 ms./homehad 35 and 41: 76 / 2 = 38. sort_bywithb.1.cmp(&a.1)sorts by average, descending, and.then(a.0.cmp(b.0))breaks ties by path so the output is stable regardless ofHashMaporder.max_by_key(|r| r.millis)finds the slowest request, andmapon the resultingOptionextracts the fields.
Idioms and pitfalls
Idiom: pass functions and methods directly
filter_map(|line| parse(line)) can be written filter_map(parse), and map(|s| s.len()) as map(str::len). Clippy's redundant_closure lint flags closures that only forward their argument to a function.
Idiom: take impl Fn for flexibility, return impl Iterator
Accept closures as impl FnMut(T) -> U (or a generic F: FnMut(T) -> U) so that callers can pass any closure or function. Return iterators as impl Iterator<Item = T> instead of collecting into a Vec the caller may not need.
Pitfall: collecting too early
let v: Vec<_> = data.iter().map(f).collect(); let total: i32 = v.iter().sum(); allocates a vector only to throw it away. Chain to the end instead: data.iter().map(f).sum(). Collect only when you need to keep the results or walk them more than once.
Pitfall: side effects inside map
map is for transforming values. Putting println! or pushes to another vector inside map hides work in a lazy adapter, which may run later, partially or not at all. Use for_each, inspect for debugging, or a for loop.
Pitfall: sum and collect without a type
let total = v.iter().sum(); fails with "type annotations needed", because sum and collect are generic over their result. Write let total: i32 = ... or .sum::<i32>().
Exercises
Exercise 1: a counter factory
Write make_counter(start, step) that returns a closure; each call to the closure returns the next number. Two counters must not share state.
Solution
The closure owns next (thanks to move) and changes it, so it is FnMut, and callers must store it in a mut variable.
fn make_counter(start: u32, step: u32) -> impl FnMut() -> u32 {
let mut next = start;
move || {
let current = next;
next += step;
current
}
}
fn main() {
let mut tickets = make_counter(100, 1);
let mut evens = make_counter(0, 2);
println!("{} {} {}", tickets(), tickets(), tickets());
println!("{} {} {}", evens(), evens(), evens());
println!("tickets again: {}", tickets());
}
100 101 102
0 2 4
tickets again: 103
Exercise 2: chain it
With iterators only, compute the sum of the squares of the odd numbers from 1 to 10, keep the words longer than three letters in upper case, and reverse the order of the words in a sentence.
Solution
fn main() {
let odd_squares: u32 = (1..=10).filter(|n| n % 2 == 1).map(|n| n * n).sum();
println!("sum of odd squares 1..=10 = {odd_squares}");
let words = ["rust", "is", "fast", "and", "safe"];
let long_caps: Vec<String> = words
.iter()
.filter(|w| w.len() > 3)
.map(|w| w.to_uppercase())
.collect();
println!("{long_caps:?}");
let sentence = words.iter().rev().copied().collect::<Vec<_>>().join(" ");
println!("{sentence}");
}
sum of odd squares 1..=10 = 165
["RUST", "FAST", "SAFE"]
safe and fast is rust
1 + 9 + 25 + 49 + 81 = 165. rev works because array iterators can be walked from both ends (they implement DoubleEndedIterator).
Exercise 3: compose two functions
Write compose(f, g) that returns a closure computing g(f(x)). Use it to build a price label that adds 18% GST to an amount in paise and formats it in rupees.
Solution
compose is generic over the three types involved and returns impl Fn(A) -> C. move transfers f and g into the returned closure.
fn compose<A, B, C>(f: impl Fn(A) -> B, g: impl Fn(B) -> C) -> impl Fn(A) -> C {
move |x| g(f(x))
}
fn main() {
let add_gst = |paise: u64| paise * 118 / 100;
let to_rupees = |paise: u64| format!("Rs {}.{:02}", paise / 100, paise % 100);
let price_label = compose(add_gst, to_rupees);
println!("{}", price_label(50_000));
println!("{}", price_label(1_999));
let shout = compose(
|s: &str| s.trim().to_string(),
|s: String| s.to_uppercase() + "!",
);
println!("{}", shout(" hello "));
}
Rs 590.00
Rs 23.58
HELLO!
50,000 × 118 / 100 = 59,000 paise = Rs 590.00. 1,999 × 118 / 100 = 2,358.82, which integer division truncates to 2,358 paise = Rs 23.58.
Exercise 4: a stepping range
Implement an iterator step_range(start, end, step) that counts from start towards end (exclusive) in steps of step, which may be negative. Check it against the standard library's step_by.
Solution
struct StepRange {
current: i64,
end: i64,
step: i64,
}
impl Iterator for StepRange {
type Item = i64;
fn next(&mut self) -> Option<i64> {
if (self.step > 0 && self.current >= self.end)
|| (self.step < 0 && self.current <= self.end)
{
return None;
}
let value = self.current;
self.current += self.step;
Some(value)
}
}
fn step_range(start: i64, end: i64, step: i64) -> StepRange {
assert!(step != 0, "step must not be zero");
StepRange {
current: start,
end,
step,
}
}
fn main() {
let up: Vec<i64> = step_range(0, 20, 5).collect();
let down: Vec<i64> = step_range(10, 0, -3).collect();
println!("{up:?}");
println!("{down:?}");
let labelled: Vec<String> = step_range(1, 4, 1)
.zip(["one", "two", "three"])
.map(|(n, w)| format!("{n}={w}"))
.collect();
println!("{labelled:?}");
let std_version: Vec<i64> = (0..20).step_by(5).collect();
println!("same as step_by: {}", up == std_version);
}
[0, 5, 10, 15]
[10, 7, 4, 1]
["1=one", "2=two", "3=three"]
same as step_by: true
Counting down from 10 by 3 gives 10, 7, 4, 1; the next value, −2, is past the end. zip works with an array directly because arrays implement IntoIterator.
Exercise 5: strict and lenient parsing
Write parse_all, which returns all numbers or the first error, and parse_valid, which returns only the numbers that parse.
Solution
The only difference is the consumer: collecting into Result<Vec<_>, _> stops at the first error, while filter_map with .ok() skips failures.
fn parse_all(items: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
items.iter().map(|s| s.trim().parse::<i32>()).collect()
}
fn parse_valid(items: &[&str]) -> Vec<i32> {
items.iter().filter_map(|s| s.trim().parse().ok()).collect()
}
fn main() {
let clean = ["12", " 7", "30 "];
let messy = ["12", "seven", "30", ""];
println!("{:?}", parse_all(&clean));
println!("{:?}", parse_all(&messy));
println!("{:?}", parse_valid(&messy));
}
Ok([12, 7, 30])
Err(ParseIntError { kind: InvalidDigit })
[12, 30]
Exercise 6: group by initial
Group a list of words by their lower-cased first letter into a BTreeMap<char, Vec<&str>> using fold, skipping empty strings.
Solution
fold threads the map through the iteration; the closure takes it by value as mut groups, adds the word and returns it. The lifetime 'a lets the result hold the original string slices.
use std::collections::BTreeMap;
fn group_by_initial<'a>(words: &[&'a str]) -> BTreeMap<char, Vec<&'a str>> {
words.iter().fold(BTreeMap::new(), |mut groups, &w| {
if let Some(first) = w.chars().next() {
groups
.entry(first.to_ascii_lowercase())
.or_insert_with(Vec::new)
.push(w);
}
groups
})
}
fn main() {
let cities = ["Pune", "Patna", "Delhi", "Agra", "Durg", "pondicherry", ""];
for (letter, names) in group_by_initial(&cities) {
println!("{letter}: {names:?}");
}
}
a: ["Agra"]
d: ["Delhi", "Durg"]
p: ["Pune", "Patna", "pondicherry"]
"pondicherry" joins "Pune" and "Patna" under p because the key is lower-cased, and the empty string has no first character, so if let skips it.
Interview questions
Q1. What is a closure, and how does it differ from a function?
A closure is an anonymous function that can capture variables from the scope where it is defined. Functions cannot capture anything and must have fully annotated signatures, while closure types are usually inferred. Each closure has a unique, unnameable type; one that captures nothing can be coerced to a function pointer.
Q2. What are the three ways a closure can capture a variable?
By shared reference when it only reads the variable, by mutable reference when it modifies it, and by value when it moves or consumes it. The compiler picks the least demanding mode the body needs. The move keyword forces capture by value for all variables.
Q3. Explain Fn, FnMut and FnOnce.
They are the traits closures implement, based on what they do with their captures. FnOnce closures may consume captured values and can be called once; FnMut closures may mutate captures and need &mut access to be called; Fn closures only read and can be called through a shared reference. Fn is a subtrait of FnMut, which is a subtrait of FnOnce, so a function asking for FnOnce accepts every closure.
Q4. What does move do, and when do you need it?
move makes a closure take ownership of every variable it captures instead of borrowing it. You need it when the closure outlives the current scope: when returning it, storing it, or passing it to thread::spawn. It does not change which Fn trait the closure implements.
Q5. How do you return a closure from a function?
Return impl Fn(A) -> B (or FnMut/FnOnce) when there is one concrete closure type, usually with move so it owns its data. When different code paths return different closures, or you need to store them in a collection, return Box<dyn Fn(A) -> B>, which costs a heap allocation and dynamic dispatch.
Q6. What does it mean that iterators are lazy?
Adapters like map and filter only build a description of the work; nothing runs until a consumer such as collect, sum or a for loop calls next. Items then flow through the entire chain one at a time, so short-circuiting consumers like find, any and take stop early, no intermediate collections are built, and infinite iterators are usable. Forgetting the consumer triggers an "unused Map that must be used" warning.
Q7. What is the difference between iter, iter_mut and into_iter?
iter yields shared references and leaves the collection usable; iter_mut yields mutable references so you can modify elements in place; into_iter on an owned collection yields owned values and consumes the collection. for x in &v, for x in &mut v and for x in v call these through the IntoIterator trait.
Q8. Are iterator chains slower than loops?
Not in optimised builds. Adapters are small generic structs and closures are monomorphised and inlined, so a chain usually compiles to the same tight loop as hand-written code, and avoiding indexing can eliminate bounds checks. In unoptimised debug builds they can be slower, so benchmark with --release.
Q9. How do you implement Iterator for your own type?
Implement the Iterator trait with an associated type Item and a fn next(&mut self) -> Option<Self::Item> that advances internal state and returns None when finished. All adapters and consumers then come for free as default methods. To make for x in &collection work, also implement IntoIterator for &YourType.
Q10. What does collect need to know, and what can it produce?
It must know the target type, given by an annotation or turbofish, because it is generic through the FromIterator trait. It can build Vec, String, HashMap, HashSet, BTreeMap and more, and collecting an iterator of Result<T, E> into Result<Vec<T>, E> returns the first error or all the values.
Q11. Why must a variable holding an FnMut closure be declared mut?
Calling an FnMut closure takes &mut self, because the call may change the state the closure captured, such as a counter. Mutable access requires a mutable binding, just as calling a &mut self method does. Fn closures are called through &self and need no mut.
Key takeaways
- Closures are anonymous functions that capture their environment; their parameter and return types are usually inferred, but each parameter gets one concrete type.
- Captures are by shared reference, mutable reference or value, whichever the body needs;
moveforces capture by value. Fnreads,FnMutmutates,FnOnceconsumes; accept the most general bound your function can work with.- Return closures as
impl Fn...orBox<dyn Fn...>, usually withmove. - An iterator is anything with
next() -> Option<Item>;forloops useIntoIterator, anditer,iter_mutandinto_itergive&T,&mut TandT. - Adapters are lazy and do nothing until a consumer pulls items; items flow through the chain one at a time.
collectbuilds anyFromIteratortype, includingResult<Vec<T>, E>; annotate the target.- In release builds, iterator chains are as fast as loops and avoid bounds checks.
Next lesson
Continue with Smart pointers in Rust.

