Skip to main content

Closures and Iterators

Closures and iterators bring functional-programming style into Rust without abandoning ownership and static dispatch. A closure is an anonymous function-like value that can capture its environment. An iterator is a value that produces a sequence of items through repeated calls to next. Together they support expressive data processing pipelines that compile to efficient loops.

This page follows generics, traits, and lifetimes because closures are represented by traits and iterators are built around the Iterator trait. It also supports the command-line project style used in the Rust book's minigrep chapter, where iterators process arguments, lines, and search results.

Definitions

A closure is written with vertical bars around parameters:

let add_one = |x| x + 1;

Closures can infer parameter and return types from use. Unlike functions, closures can capture variables from the scope where they are defined.

Closures capture environment in one of three ways: by immutable borrow, by mutable borrow, or by value. These correspond to the closure traits Fn, FnMut, and FnOnce. A closure that moves a captured value out of itself can be called only once.

The move keyword forces a closure to take ownership of captured values. This is common when spawning threads because the closure may outlive the current stack frame.

An iterator is any type implementing the Iterator trait:

trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}

Calling next returns Some(item) until the sequence is exhausted, then returns None.

Iterator adapters, such as map, filter, and take, transform iterators lazily. Consuming adapters, such as sum, collect, and for_each, drive iteration and produce a final value or side effect.

Key results

The first key result is that closures capture only what they use. If a closure only reads a value, it can capture by immutable borrow. If it mutates captured state, it needs a mutable borrow. If it consumes captured state, it captures by value.

The second key result is that iterator adapters are lazy. Writing v.iter().map(|x| x + 1); does no work unless a consuming adapter or for loop pulls values from the iterator.

The third key result is that iterators often replace manual indexing. They reduce bounds errors and make intent clearer: transform, filter, fold, collect.

The fourth key result is that Rust can optimize iterator chains heavily. The book emphasizes that high-level iterator code can compile to code comparable to hand-written loops, because the abstractions are resolved statically in many common cases.

Proof sketch for laziness: map returns a new iterator value that stores the original iterator and the closure. It does not call the closure immediately. The closure is called only when next is called on the mapped iterator, or when a consumer such as collect repeatedly calls next.

A related result is that iterator chains separate "what sequence should be produced" from "when is it demanded." This makes many transformations easier to read, but it also means side effects inside adapters should be treated carefully. A map closure that prints text will not print anything until the iterator is consumed. If the purpose is side effects, a for loop or for_each is usually clearer.

The capture traits explain why closure types appear in APIs. A function that accepts F: Fn() promises it will only call the closure through shared access. A function that accepts F: FnMut() may call a closure that changes captured state. A function that accepts F: FnOnce() may consume the closure. Thread spawning uses FnOnce because the closure is run once, and it may need to move captured values into the new thread. These trait bounds are the type-level form of the same capture behavior seen in examples.

Iterators also make ownership flow visible. iter lets many read-only pipelines inspect data without consuming it. iter_mut supports in-place updates while preserving the collection. into_iter consumes the collection and is often the right choice when the pipeline should produce owned output. Choosing the wrong entry point is a common source of move or borrow errors, but the fix is usually conceptual: decide whether the pipeline should read, mutate, or take ownership.

Because adapters are composable, it is tempting to make every loop a chain. The better rule is readability. A short transform-filter-collect pipeline is often excellent. A chain with hidden side effects, complex branching, and hard-to-name closures may be clearer as a for loop.

Iterator return types can also guide API design. Returning Vec<T> says the function eagerly produced owned results. Returning impl Iterator<Item = T> says the caller can consume a generated sequence lazily. Returning references from an iterator ties the iterator to borrowed input. Each choice has lifetime and ownership consequences, so iterator-heavy code should still be read through the same ownership lens as the rest of Rust.

This is why iterator examples should always identify the item type. Many confusing compiler errors become straightforward once you know whether the closure receives T, &T, or &mut T.

After that, ownership of the pipeline is usually clear.

Name intermediate iterators when clarity improves.

Then optimize only after measuring.

Visual

Adapter kindExamplesRuns immediately?Output
Sourceiter, into_iter, iter_mutnoiterator
Transforming adaptermap, filter, take, skipnoiterator
Combining adapterzip, chainnoiterator
Consuming adaptersum, collect, count, for_eachyesfinal value or effect
Manual pullnextone item at a timeOption<Item>

Worked example 1: closure capture modes

Problem: determine how three closures capture surrounding variables.

  1. Immutable read:
let list = vec![1, 2, 3];
let only_reads = || println!("{list:?}");
only_reads();
println!("{list:?}");

The closure only prints list, so it captures by immutable borrow. The original list remains usable for reading after the closure call.

  1. Mutable update:
let mut list = vec![1, 2, 3];
let mut adds = || list.push(4);
adds();

The closure mutates list, so the closure itself must be mutable and holds a mutable borrow while it exists. You cannot read list between defining adds and the last use of adds if that would overlap the mutable borrow.

  1. Forced move:
let list = vec![1, 2, 3];
let owns_list = move || println!("{list:?}");
owns_list();

The move keyword transfers ownership of list into the closure. The outer binding cannot be used afterward.

  1. Check the answer. The capture mode is determined by closure body needs, except move forces ownership capture. These rules are the same ownership rules applied to anonymous function values.

Worked example 2: computing filtered squares with iterators

Problem: from [1, 2, 3, 4, 5, 6], keep even numbers, square them, and sum the squares.

  1. Start with a vector:
let numbers = vec![1, 2, 3, 4, 5, 6];
  1. Borrow each element:
numbers.iter()

This produces &i32 items.

  1. Filter evens:
.filter(|n| *n % 2 == 0)

Because the closure receives references, *n gets the integer value for arithmetic.

  1. Square:
.map(|n| n * n)

After filtering, n is still a reference in this context, but multiplication works through deref coercions for references to integers in this expression. Writing (*n) * (*n) would be more explicit.

  1. Sum:
.sum::<i32>()
  1. Check manually. The even numbers are 2, 4, and 6. Their squares are 4, 16, and 36. The sum is 56.

The key is that filter and map define a pipeline, while sum actually consumes it.

Code

#[derive(Debug, PartialEq)]
struct Shoe {
size: u32,
style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes
.into_iter()
.filter(|shoe| shoe.size == shoe_size)
.collect()
}

fn main() {
let inventory = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];

let my_size = shoes_in_size(inventory, 10);
println!("{my_size:?}");
}

This example consumes the input vector with into_iter, keeps matching shoes, and collects the owned shoes into a new vector. The original inventory is moved because the result owns selected shoes.

Common pitfalls

  • Creating an iterator adapter chain and forgetting a consuming adapter, resulting in no work.
  • Confusing iter, iter_mut, and into_iter. They yield shared references, mutable references, and owned values respectively.
  • Expecting a closure to have independent type annotations for multiple uses. A closure's inferred types are fixed after first use.
  • Forgetting mut on a closure binding that mutates captured state.
  • Adding move and then trying to use the moved variable outside the closure.
  • Using manual indexing when iterator methods would avoid bounds logic.
  • Calling collect without enough type context. Add an annotation such as let v: Vec<_> = ...collect();.

Connections