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

FeatureFunctionClosure
Syntaxfn name(x: i32) -> i32|x| x + 1
NamedYesOptional
Captures variablesNoYes
Type annotationsRequiredUsually 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:

MethodWhat It DoesOwnership
.iter()Borrows each element as &TCollection still usable
.iter_mut()Borrows each element as &mut TCan modify elements
.into_iter()Takes ownership of each elementCollection 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

MethodWhat It DoesReturns
map(|x| ...)Transform each elementIterator
filter(|x| ...)Keep matching elementsIterator
fold(init, |acc, x| ...)Reduce to single valueValue
sum()Sum all elementsValue
count()Count elementsusize
collect()Gather into collectionCollection
any(|x| ...)True if any matchbool
all(|x| ...)True if all matchbool
find(|x| ...)First matching elementOption
enumerate()Add index to each elementIterator
zip(other)Pair with another iteratorIterator
take(n)First n elementsIterator
skip(n)Skip first n elementsIterator
flatten()Flatten nested iteratorsIterator
chain(other)Concatenate two iteratorsIterator

Summary

ConceptSyntaxPurpose
Closure|x| x + 1Anonymous function
Fnimpl Fn(i32) -> i32Borrows captures immutably
FnMutimpl FnMut(i32)Borrows captures mutably
FnOnceimpl FnOnce() -> StringTakes ownership of captures
movemove || ...Force ownership transfer
iter()vec.iter()Borrow elements
into_iter()vec.into_iter()Consume collection
iter_mut()vec.iter_mut()Mutable borrow elements
Custom iteratorimpl Iterator for TypeYour own iterator type

Source Code

View source code on GitHub ->

What’s Next?

We now have closures and iterators — two tools you will use in almost every Rust program. Next, we learn smart pointersBox, 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.

Next: Rust Tutorial #13: Smart Pointers