Skip to main content

Iterators & Closures

Closures are anonymous functions you can store in variables and pass around. Iterators provide a way to process sequences of elements lazily and efficiently. Together, they enable a functional programming style that is both expressive and zero-cost in Rust.

Closures​

A closure is a function without a name, defined inline with |parameters| body:

fn main() {
let add = |a, b| a + b;
let greet = |name: &str| format!("Hello, {name}!");

println!("{}", add(3, 4)); // 7
println!("{}", greet("Alice")); // Hello, Alice!
}

Unlike regular functions, closures:

  • Can capture variables from their surrounding scope
  • Infer parameter and return types (explicit annotations are optional)
  • Are each their own unique, anonymous type

Capturing variables​

Closures can use variables from the enclosing scope:

fn main() {
let multiplier = 3;
let multiply = |x| x * multiplier;

println!("{}", multiply(5)); // 15
println!("{}", multiply(10)); // 30
}

multiply captures multiplier by reference. The closure borrows multiplier for as long as it is used.

How closures capture​

Closures capture in three ways, from least to most restrictive:

Capture modeTraitWhen used
By referenceFnClosure only reads the captured value
By mutable refFnMutClosure modifies the captured value
By value (move)FnOnceClosure takes ownership

Rust automatically chooses the least restrictive mode needed:

fn main() {
let name = String::from("Alice");

// Captures by reference (Fn) - only reads name
let greet = || println!("Hello, {name}!");
greet();
greet(); // Can call multiple times
println!("{name}"); // name is still valid

// Captures by mutable reference (FnMut)
let mut count = 0;
let mut increment = || {
count += 1;
count
};
println!("{}", increment()); // 1
println!("{}", increment()); // 2

// Captures by value with move
let data = vec![1, 2, 3];
let owns_data = move || {
println!("{:?}", data);
};
owns_data();
// println!("{:?}", data); // Error: data was moved into the closure
}

The move keyword forces a closure to take ownership of all captured variables. This is essential for threads (chapter 16) where data must be moved to another thread.

Closures as function parameters​

Use the Fn traits to accept closures:

fn apply_twice<F: Fn(i32) -> i32>(f: F, value: i32) -> i32 {
f(f(value))
}

fn main() {
let double = |x| x * 2;
let add_three = |x| x + 3;

println!("{}", apply_twice(double, 5)); // 20
println!("{}", apply_twice(add_three, 5)); // 11
}
TraitMeaningCalling
FnCan be called multiple times, no mutationMost flexible
FnMutCan be called multiple times, may mutateMutable access
FnOnceCan be called only once (consumes captured data)Most restrictive

The hierarchy: every Fn is also FnMut, and every FnMut is also FnOnce.

Tip: Accept Fn when possible, FnMut if the closure needs to mutate, and FnOnce if it needs to take ownership. This gives callers maximum flexibility.

Returning closures​

fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}

fn main() {
let add_five = make_adder(5);
println!("{}", add_five(10)); // 15
println!("{}", add_five(20)); // 25
}

Iterators​

An iterator produces a sequence of values one at a time. The Iterator trait has one required method:

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

Each call to next() returns Some(value) or None when the sequence is exhausted.

Creating iterators​

fn main() {
let numbers = vec![1, 2, 3, 4, 5];

let mut iter = numbers.iter();

println!("{:?}", iter.next()); // Some(1)
println!("{:?}", iter.next()); // Some(2)
println!("{:?}", iter.next()); // Some(3)
println!("{:?}", iter.next()); // Some(4)
println!("{:?}", iter.next()); // Some(5)
println!("{:?}", iter.next()); // None
}

iter vs into_iter vs iter_mut​

This is a common source of confusion:

MethodYieldsConsumes collection?Use when
.iter()&TNoYou need to read elements
.iter_mut()&mut TNoYou need to modify elements
.into_iter()TYesYou need owned elements
fn main() {
let numbers = vec![1, 2, 3];

// .iter() - borrows, collection stays valid
for n in numbers.iter() {
println!("{n}"); // n is &i32
}
println!("Still valid: {:?}", numbers);

// .iter_mut() - mutable borrow
let mut numbers = vec![1, 2, 3];
for n in numbers.iter_mut() {
*n *= 10; // n is &mut i32
}
println!("Modified: {:?}", numbers);

// .into_iter() - takes ownership
let numbers = vec![1, 2, 3];
for n in numbers.into_iter() {
println!("{n}"); // n is i32 (owned)
}
// numbers is no longer valid
}

Note: A for loop calls .into_iter() implicitly. Writing for n in numbers is the same as for n in numbers.into_iter(). Use for n in &numbers for .iter() and for n in &mut numbers for .iter_mut().

Iterator adaptors​

Adaptors transform an iterator into another iterator. They are lazy - they do nothing until consumed.

map - transform each element​

fn main() {
let numbers = vec![1, 2, 3, 4, 5];

let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();

println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}

filter - keep elements matching a predicate​

fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let evens: Vec<i32> = numbers.iter().copied().filter(|n| n % 2 == 0).collect();

println!("{:?}", evens); // [2, 4, 6, 8, 10]
}

Chaining adaptors​

The real power is chaining multiple operations:

fn main() {
let words = vec!["hello", "world", "foo", "bar", "rust"];

let result: Vec<String> = words
.iter()
.filter(|w| w.len() > 3)
.map(|w| w.to_uppercase())
.collect();

println!("{:?}", result); // ["HELLO", "WORLD", "RUST"]
}

Common adaptors​

AdaptorWhat it does
map(f)Transform each element
filter(f)Keep elements where f returns true
enumerate()Pair each element with its index
zip(other)Pair elements from two iterators
take(n)Take the first n elements
skip(n)Skip the first n elements
chain(other)Concatenate two iterators
flatten()Flatten nested iterators
flat_map(f)Map then flatten
peekable()Allow peeking at the next element
inspect(f)Call f on each element for debugging
rev()Reverse (requires DoubleEndedIterator)

Consuming iterators​

Adaptors are lazy. Consumers drive the iteration and produce a final value.

collect - gather into a collection​

fn main() {
let chars: Vec<char> = "hello".chars().collect();
println!("{:?}", chars); // ['h', 'e', 'l', 'l', 'o']

let csv = vec!["Alice", "Bob", "Charlie"];
let joined: String = csv.into_iter().collect::<Vec<&str>>().join(", ");
println!("{joined}"); // Alice, Bob, Charlie
}

collect can produce many types - Vec, String, HashMap, HashSet, and more.

fold - reduce to a single value​

fn main() {
let numbers = vec![1, 2, 3, 4, 5];

let sum = numbers.iter().fold(0, |acc, n| acc + n);
let product = numbers.iter().fold(1, |acc, n| acc * n);

println!("Sum: {sum}"); // 15
println!("Product: {product}"); // 120
}

fold takes an initial accumulator value and a closure that combines the accumulator with each element.

Other consumers​

ConsumerWhat it does
collect()Gather into a collection
fold(init, f)Reduce to a single value
sum()Sum all elements (requires Sum trait)
product()Multiply all elements
count()Count elements
any(f)True if any element satisfies f
all(f)True if all elements satisfy f
find(f)First element matching f (returns Option)
position(f)Index of first match (returns Option<usize>)
min() / max()Smallest / largest element
for_each(f)Call f on each element (like a for loop)
fn main() {
let numbers = vec![1, 2, 3, 4, 5];

println!("Sum: {}", numbers.iter().sum::<i32>());
println!("Any even? {}", numbers.iter().any(|n| n % 2 == 0));
println!("All positive? {}", numbers.iter().all(|n| *n > 0));
println!("First even: {:?}", numbers.iter().copied().find(|n| n % 2 == 0));
println!("Max: {:?}", numbers.iter().max());
}

Lazy evaluation​

Adaptors do not execute until a consumer drives the iterator. This means no intermediate collections are created:

fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Nothing happens here - the chain is lazy
let iter = numbers.iter().copied().filter(|n| n % 2 == 0).map(|n| n * 10);

// Only now does the computation happen
let result: Vec<i32> = iter.collect();
println!("{:?}", result); // [20, 40, 60, 80, 100]
}

This is efficient - Rust processes each element through the entire chain before moving to the next, avoiding intermediate Vec allocations.

A complete example​

Processing a list of students:

#[derive(Debug)]
struct Student {
name: String,
grade: u32,
}

fn main() {
let students = vec![
Student { name: String::from("Alice"), grade: 92 },
Student { name: String::from("Bob"), grade: 67 },
Student { name: String::from("Charlie"), grade: 85 },
Student { name: String::from("Diana"), grade: 91 },
Student { name: String::from("Eve"), grade: 73 },
];

let honor_roll: Vec<&str> = students
.iter()
.filter(|s| s.grade >= 85)
.map(|s| s.name.as_str())
.collect();

let average: f64 = students.iter().map(|s| s.grade as f64).sum::<f64>()
/ students.len() as f64;

let highest = students.iter().max_by_key(|s| s.grade).unwrap();

println!("Honor roll: {:?}", honor_roll);
println!("Average grade: {average:.1}");
println!("Highest: {} ({})", highest.name, highest.grade);
}
Honor roll: ["Alice", "Charlie", "Diana"]
Average grade: 81.6
Highest: Alice (92)

Summary​

  • Closures are anonymous functions defined with |params| body
  • Closures capture variables from their scope by reference, mutable reference, or ownership (move)
  • The Fn, FnMut, and FnOnce traits describe how closures capture and how often they can be called
  • Iterators produce sequences via the next() method
  • .iter() borrows, .iter_mut() mutably borrows, .into_iter() takes ownership
  • Adaptors (map, filter, take, etc.) are lazy - they build a chain of transformations
  • Consumers (collect, fold, sum, find, etc.) drive the iteration and produce results
  • Chains of adaptors are zero-cost - no intermediate collections

Next up: Project: CLI Task Manager - putting everything together to build a complete command-line application with argument parsing, file storage, and proper error handling.