In the previous tutorial, we learned lifetimes. Now we learn two features that make Rust code clean and expressive: closures and iterators.
Closures are anonymous functions you can store in variables and pass to other functions. Iterators let you process collections step by step. Together, they replace most loops with short, readable code.
What Is a Closure?
A closure is a function without a name. It can capture variables from its environment:
fn main() {
let add = |a, b| a + b;
let multiply = |a, b| a * b;
println!("{}", add(3, 4)); // 7
println!("{}", multiply(3, 4)); // 12
}
The syntax is |parameters| body. If the body is one expression, you do not need curly braces. If it is longer:
let greet = |name: &str| {
let message = format!("Hello, {}!", name);
println!("{}", message);
};
greet("Alex");
Closures vs Functions
| Feature | Function | Closure |
|---|---|---|
| Syntax | fn name(x: i32) -> i32 | |x| x + 1 |
| Named | Yes | Optional |
| Captures variables | No | Yes |
| Type annotations | Required | Usually inferred |
The biggest difference is that closures can capture variables from the surrounding scope:
let offset = 10;
let add_offset = |x| x + offset; // Captures offset
println!("{}", add_offset(5)); // 15
A regular function cannot do this. It only has access to its parameters and global items.
Passing Closures to Functions
You can write functions that accept closures as parameters:
fn apply_operation(a: i32, b: i32, op: impl Fn(i32, i32) -> i32) -> i32 {
op(a, b)
}
fn main() {
let result = apply_operation(10, 5, |a, b| a + b);
println!("{}", result); // 15
let result = apply_operation(10, 5, |a, b| a * b);
println!("{}", result); // 50
}
The impl Fn(i32, i32) -> i32 means “any closure or function that takes two i32 values and returns an i32.”
Here is another example — applying a closure to every element:
fn apply_to_vec(numbers: &[i32], op: impl Fn(i32) -> i32) -> Vec<i32> {
numbers.iter().map(|&n| op(n)).collect()
}
fn main() {
let doubled = apply_to_vec(&[1, 2, 3, 4], |x| x * 2);
println!("{:?}", doubled); // [2, 4, 6, 8]
let incremented = apply_to_vec(&[10, 20, 30], |x| x + 1);
println!("{:?}", incremented); // [11, 21, 31]
}
The Three Closure Traits: Fn, FnMut, FnOnce
Rust has three traits for closures, based on how they use captured variables:
Fn — Borrows Immutably
The closure only reads the captured variables. It can be called many times:
let name = String::from("Alex");
let greet = || println!("Hello, {}!", name); // Borrows name
greet();
greet(); // Can call multiple times
println!("{}", name); // name is still valid
FnMut — Borrows Mutably
The closure modifies captured variables. It can be called many times but needs mut:
fn apply_mut<F>(times: usize, mut f: F)
where
F: FnMut(usize),
{
for i in 0..times {
f(i);
}
}
fn main() {
let mut results = Vec::new();
apply_mut(5, |i| {
results.push(i * i);
});
println!("{:?}", results); // [0, 1, 4, 9, 16]
}
The closure captures results and modifies it. The function parameter uses FnMut because the closure mutates captured state.
FnOnce — Takes Ownership
The closure takes ownership of captured variables. It can only be called once:
fn consume_and_print<F>(f: F)
where
F: FnOnce() -> String,
{
println!("{}", f());
// f(); // ERROR: cannot call again — it consumed its captured values
}
fn main() {
let name = String::from("Alex");
consume_and_print(|| {
format!("Hello, {}!", name) // name is moved into the closure
});
}
Every Fn is also FnMut, and every FnMut is also FnOnce. So if a function takes FnOnce, you can pass any closure. If it takes Fn, you can only pass closures that do not mutate or consume.
Move Closures
The move keyword forces a closure to take ownership of all captured variables:
fn main() {
let name = String::from("Alex");
let greeting = move || {
format!("Hello, {}!", name)
};
// println!("{}", name); // ERROR: name was moved into the closure
println!("{}", greeting());
}
Move closures are essential when passing closures to threads (we will cover this in the concurrency tutorial):
let data = vec![1, 2, 3];
let handle = std::thread::spawn(move || {
println!("{:?}", data); // data is owned by the thread now
});
handle.join().unwrap();
Returning Closures
Functions can return closures using impl Fn:
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
fn main() {
let add_five = make_adder(5);
let double = make_multiplier(2);
println!("{}", add_five(10)); // 15
println!("{}", double(10)); // 20
}
The returned closure must use move because it captures x (or factor). Without move, the variable would be dropped when the function returns, and the closure would hold a dangling reference.
Iterators
An iterator produces a sequence of values, one at a time. In Rust, iterators are lazy — they do not compute anything until you consume them.
Creating Iterators
Every collection has methods to create iterators:
| Method | What It Does | Ownership |
|---|---|---|
.iter() | Borrows each element as &T | Collection still usable |
.iter_mut() | Borrows each element as &mut T | Can modify elements |
.into_iter() | Takes ownership of each element | Collection is consumed |
let words = vec![String::from("hello"), String::from("world")];
// iter() — borrows, vec still usable
for word in words.iter() {
println!("Borrowed: {}", word);
}
println!("Vec still here: {:?}", words);
// iter_mut() — mutable borrow
let mut scores = vec![80, 90, 70];
for score in scores.iter_mut() {
*score += 10; // Add bonus
}
println!("With bonus: {:?}", scores); // [90, 100, 80]
// into_iter() — consumes the vec
let items = vec![1, 2, 3];
let doubled: Vec<i32> = items.into_iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6]
// items is gone — into_iter consumed it
Iterator Methods
Iterator methods are called adaptors (they transform iterators) and consumers (they produce a final value).
map — Transform Each Element
let numbers = vec![1, 2, 3, 4, 5];
let squares: Vec<i32> = numbers.iter().map(|&x| x * x).collect();
println!("{:?}", squares); // [1, 4, 9, 16, 25]
filter — Keep Only Matching Elements
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
println!("{:?}", evens); // [2, 4, 6]
fold — Reduce to a Single Value
let numbers = vec![1, 2, 3, 4, 5];
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
println!("{}", sum); // 15
fold takes an initial value (0) and a closure that combines the accumulator with each element.
sum — Quick Sum
let total: i32 = vec![1, 2, 3, 4, 5].iter().sum();
println!("{}", total); // 15
collect — Gather Results into a Collection
let words = vec!["hello", "world"];
let lengths: Vec<usize> = words.iter().map(|w| w.len()).collect();
println!("{:?}", lengths); // [5, 5]
collect is flexible — it can create Vec, HashMap, String, and more. You usually need a type annotation so Rust knows what to collect into.
Chaining Iterators
The real power comes from chaining multiple adaptors:
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: i32 = numbers
.iter()
.filter(|&&x| x % 2 == 0) // Keep even numbers: 2, 4, 6, 8, 10
.map(|&x| x * x) // Square them: 4, 16, 36, 64, 100
.sum(); // Sum: 220
println!("{}", result); // 220
This reads like a pipeline: take numbers, filter, transform, sum. Compare this to the equivalent loop:
let mut result = 0;
for &x in &numbers {
if x % 2 == 0 {
result += x * x;
}
}
The iterator version is shorter and harder to get wrong. And thanks to Rust’s zero-cost abstractions, both versions compile to the same machine code.
Practical Iterator Functions
Here are useful functions built with iterators:
fn sum_of_squares(numbers: &[i32]) -> i32 {
numbers.iter().map(|&x| x * x).sum()
}
fn even_numbers(numbers: &[i32]) -> Vec<i32> {
numbers.iter().filter(|&&x| x % 2 == 0).copied().collect()
}
fn word_lengths(words: &[&str]) -> Vec<usize> {
words.iter().map(|w| w.len()).collect()
}
fn flatten_and_sort(nested: &[Vec<i32>]) -> Vec<i32> {
let mut result: Vec<i32> = nested.iter().flatten().copied().collect();
result.sort();
result
}
println!("{}", sum_of_squares(&[1, 2, 3, 4])); // 30
println!("{:?}", even_numbers(&[1, 2, 3, 4, 5, 6])); // [2, 4, 6]
println!("{:?}", word_lengths(&["hello", "world", "hi"])); // [5, 5, 2]
let nested = vec![vec![3, 1], vec![4, 2]];
println!("{:?}", flatten_and_sort(&nested)); // [1, 2, 3, 4]
Custom Iterators
You can implement the Iterator trait for your own types:
struct Counter {
current: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Counter {
Counter { current: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.current < self.max {
self.current += 1;
Some(self.current)
} else {
None
}
}
}
You only need to implement next(). All other iterator methods (.map(), .filter(), .sum(), etc.) come for free:
let counter = Counter::new(5);
let values: Vec<u32> = counter.collect();
println!("{:?}", values); // [1, 2, 3, 4, 5]
// Use all iterator methods
let sum: u32 = Counter::new(5).sum();
println!("{}", sum); // 15
let doubled: Vec<u32> = Counter::new(3).map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6]
Fibonacci Iterator
Here is a more interesting custom iterator:
struct Fibonacci {
a: u64,
b: u64,
count: usize,
max: usize,
}
impl Fibonacci {
fn new(max: usize) -> Fibonacci {
Fibonacci { a: 0, b: 1, count: 0, max }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
if self.count >= self.max {
return None;
}
let result = self.a;
let next = self.a + self.b;
self.a = self.b;
self.b = next;
self.count += 1;
Some(result)
}
}
fn main() {
let fib: Vec<u64> = Fibonacci::new(10).collect();
println!("{:?}", fib); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}
Iterators Are Zero-Cost
Rust iterators compile down to the same machine code as hand-written loops. The compiler inlines the closures and optimizes everything away. This is called zero-cost abstraction — you pay nothing for the clean syntax.
In benchmarks, iterator chains are often faster than hand-written loops because the compiler can optimize the whole pipeline at once.
Common Iterator Methods Reference
| Method | What It Does | Returns |
|---|---|---|
map(|x| ...) | Transform each element | Iterator |
filter(|x| ...) | Keep matching elements | Iterator |
fold(init, |acc, x| ...) | Reduce to single value | Value |
sum() | Sum all elements | Value |
count() | Count elements | usize |
collect() | Gather into collection | Collection |
any(|x| ...) | True if any match | bool |
all(|x| ...) | True if all match | bool |
find(|x| ...) | First matching element | Option |
enumerate() | Add index to each element | Iterator |
zip(other) | Pair with another iterator | Iterator |
take(n) | First n elements | Iterator |
skip(n) | Skip first n elements | Iterator |
flatten() | Flatten nested iterators | Iterator |
chain(other) | Concatenate two iterators | Iterator |
Summary
| Concept | Syntax | Purpose |
|---|---|---|
| Closure | |x| x + 1 | Anonymous function |
| Fn | impl Fn(i32) -> i32 | Borrows captures immutably |
| FnMut | impl FnMut(i32) | Borrows captures mutably |
| FnOnce | impl FnOnce() -> String | Takes ownership of captures |
| move | move || ... | Force ownership transfer |
| iter() | vec.iter() | Borrow elements |
| into_iter() | vec.into_iter() | Consume collection |
| iter_mut() | vec.iter_mut() | Mutable borrow elements |
| Custom iterator | impl Iterator for Type | Your own iterator type |
Source Code
Related Tutorials
- Rust Tutorial #11: Lifetimes — previous tutorial
- Rust Tutorial #13: Smart Pointers — next tutorial
What’s Next?
We now have closures and iterators — two tools you will use in almost every Rust program. Next, we learn smart pointers — Box, Rc, Arc, and RefCell. These types give you more control over how data is stored and shared. They are essential for building complex data structures like trees and graphs.