PHASE 2 ← Back to Course
8 / 17
🔄

Iterators and Closures

Functional programming in Rust, closures, iterator adaptors, and zero-cost abstractions.

1

Closure Syntax and Capturing

Closures are anonymous functions that can capture variables from their enclosing scope. They use a compact |params| body syntax and can infer parameter and return types from context.

Rust
// Basic closure syntax
let add_one = |x| x + 1;
println!("{}", add_one(5)); // 6
// Closure with explicit types
let add = |x: i32, y: i32| -> i32 { x + y };
println!("{}", add(2, 3)); // 5
// Capturing by reference (immutable)
let x = 5;
let add_x = |y| x + y; // Borrows x immutably
println!("{}", add_x(3));           // 8
println!("x is still: {}", x); // x is still valid
Rust
// Capturing by mutable reference
let mut counter = 0;
let mut increment = || {
    counter += 1;
    counter
};
println!("{}", increment()); // 1
println!("{}", increment()); // 2
// Taking ownership (move)
let s = String::from("hello");
let take_ownership = move || println!("String: {}", s);
// println!("{}", s); // Error: s moved into closure
take_ownership();
// Complex capturing example
let x = vec![1, 2, 3];
let y = vec![4, 5, 6];
let use_x = || println!("{:?}", x); // Borrows x immutably
let use_y = move || println!("{:?}", y); // Moves y
use_x();
use_y();
// Returning closures
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    let num = 5;
    Box::new(move |x| x + num)
let f = returns_closure();
println!("{}", f(10)); // 15
💡

Capture Modes

Closures capture variables in the least restrictive way possible: immutable borrow first, then mutable borrow, and finally move (ownership). Use the move keyword to force ownership transfer, which is essential when returning closures or sending them to other threads.

2

Fn, FnMut, FnOnce Traits

Every closure in Rust implements one or more of three traits, depending on how it captures variables: FnOnce (consumes captured values), FnMut (mutates captured values), and Fn (borrows immutably).

Rust
// FnOnce: consumes captured values (can be called only once)
let s = String::from("hello");
let fn_once = move || println!("String: {}", s);
fn_once(); // Takes ownership of s
// fn_once(); // Error: already called
// FnMut: can mutate captured values
let mut counter = 0;
let mut fn_mut = || {
    counter += 1;
    counter
};
println!("{}", fn_mut()); // 1
println!("{}", fn_mut()); // 2
println!("Counter: {}", counter); // 2
// Fn: cannot mutate, borrows immutably
let x = 5;
let fn_trait = || println!("x: {}", x);
fn_trait();
fn_trait();
println!("x: {}", x); // Still accessible
Rust
// Using function traits as bounds
fn apply_once<F>(f: F)
where
    F: FnOnce(),
{
    f();
fn apply_twice<F>(mut f: F)
where
    F: FnMut(),
{
    f();
    f();
fn apply_thrice<F>(f: F)
where
    F: Fn(),
{
    f();
    f();
    f();
let s = String::from("move me");
apply_once(move || println!("{}", s));
let mut n = 0;
apply_twice(|| n += 1);
println!("n: {}", n); // 2
apply_thrice(|| println!("Hello"));
Rust
// Real-world example: event handler
struct Button {
    click_handlers: Vec<Box<dyn Fn()>>,
impl Button {
    fn new() -> Self {
        Button { click_handlers: Vec::new() }
    }
    fn on_click<F>(&mut self, handler: F)
    where
        F: Fn() + 'static,
    {
        self.click_handlers.push(Box::new(handler));
    }
    fn click(&self) {
        for handler in &self.click_handlers {
            handler();
        }
    }
let mut button = Button::new();
button.on_click(|| println!("Button clicked!"));
button.on_click(|| println!("Second handler!"));
button.click();
⚠️

Trait Hierarchy: Fn ⊂ FnMut ⊂ FnOnce

Every Fn closure also implements FnMut and FnOnce. Every FnMut closure also implements FnOnce. When accepting closures, use the least restrictive trait that works: prefer Fn when possible, then FnMut, and only use FnOnce when the closure must consume values.

3

Iterator Trait and Iterator Adaptors

The Iterator trait requires implementing just one method: next(). From that single method, you gain access to dozens of powerful adaptor methods like map, filter, fold, and more.

Rust
// Iterator trait: implements next()
let v = vec![1, 2, 3];
let mut iter = v.iter();
println!("{:?}", iter.next()); // Some(&1)
println!("{:?}", iter.next()); // Some(&2)
println!("{:?}", iter.next()); // Some(&3)
println!("{:?}", iter.next()); // None
// map() - transforms elements
let v = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
// filter() - keeps matching elements
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
// Chaining adaptors
let result: Vec<i32> = vec![1, 2, 3, 4, 5]
    .iter()
    .filter(|x| x % 2 == 0)
    .map(|x| x * x)
    .collect();
println!("{:?}", result); // [4, 16]
Rust
// zip() - combines iterators
let a = vec![1, 2, 3];
let b = vec!["a", "b", "c"];
let zipped: Vec<(i32, &str)> = a.iter()
    .cloned()
    .zip(b.iter().cloned())
    .collect();
// chain() - combines sequences
let a = vec![1, 2, 3];
let b = vec![4, 5, 6];
let combined: Vec<i32> = a.iter().chain(b.iter()).cloned().collect();
// enumerate() - adds indices
for (idx, val) in vec!["a", "b", "c"].iter().enumerate() {
    println!("{}: {}", idx, val);
// take() and skip()
let numbers = vec![1, 2, 3, 4, 5];
let first_three: Vec<i32> = numbers.iter().take(3).cloned().collect();
let skip_two: Vec<i32> = numbers.iter().skip(2).cloned().collect();
// rev() - reverse iterator
let numbers = vec![1, 2, 3];
for n in numbers.iter().rev() {
    println!("{}", n); // 3, 2, 1
}
Rust
// fold() - accumulate values
let numbers = vec![1, 2, 3, 4];
let sum = numbers.iter().fold(0, |acc, x| acc + x);
println!("{}", sum); // 10
// sum() and product()
let numbers = vec![1, 2, 3, 4];
let sum: i32 = numbers.iter().sum();
let product: i32 = numbers.iter().product();
// any() and all()
let numbers = vec![1, 2, 3, 4, 5];
let has_even = numbers.iter().any(|x| x % 2 == 0);
let all_positive = numbers.iter().all(|x| x > &0);
// find() - returns first matching element
let numbers = vec![1, 2, 3, 4, 5];
let found = numbers.iter().find(|x| *x > 3);
println!("{:?}", found); // Some(&4)

Lazy Evaluation

Iterator adaptors like map and filter are lazy, they do nothing until a consuming method like collect(), sum(), or for_each() drives the iteration. This means you can chain many adaptors without creating intermediate collections.

4

Creating Custom Iterators

Any type can become an iterator by implementing the Iterator trait with its single required method: next(). This unlocks all built-in adaptor methods automatically.

Rust
// Custom iterator: count up to n
struct Counter {
    count: u32,
    max: u32,
impl Counter {
    fn new(max: u32) -> Counter {
        Counter { count: 0, max }
    }
impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= self.max {
            Some(self.count)
        } else {
            None
        }
    }
let counter = Counter::new(5);
for value in counter {
    println!("{}", value); // 1, 2, 3, 4, 5
}
Rust
// Custom iterator: range
struct MyRange {
    start: i32,
    end: i32,
impl Iterator for MyRange {
    type Item = i32;
    fn next(&mut self) -> Option<Self::Item> {
        if self.start < self.end {
            let value = self.start;
            self.start += 1;
            Some(value)
        } else {
            None
        }
    }
let range = MyRange { start: 1, end: 5 };
let values: Vec<i32> = range.collect();
// Custom iterator: pairs from vec
struct Pairs<'a> {
    items: &'a [i32],
    index: usize,
impl<'a> Iterator for Pairs<'a> {
    type Item = (i32, i32);
    fn next(&mut self) -> Option<Self::Item> {
        if self.index + 1 < self.items.len() {
            let pair = (self.items[self.index], self.items[self.index + 1]);
            self.index += 2;
            Some(pair)
        } else {
            None
        }
    }
let nums = vec![1, 2, 3, 4, 5];
let pairs = Pairs { items: &nums, index: 0 };
for (a, b) in pairs {
    println!("{}, {}", a, b);
}
Rust
// Double-ended iterator (DoubleEndedIterator)
struct BiCounter {
    start: u32,
    end: u32,
impl Iterator for BiCounter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        if self.start <= self.end {
            let val = self.start;
            self.start += 1;
            Some(val)
        } else {
            None
        }
    }
impl DoubleEndedIterator for BiCounter {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.start <= self.end {
            let val = self.end;
            self.end -= 1;
            Some(val)
        } else {
            None
        }
    }
let bi = BiCounter { start: 1, end: 5 };
let values: Vec<u32> = bi.collect();
println!("{:?}", values); // [1, 2, 3, 4, 5]
💡

DoubleEndedIterator

Implementing DoubleEndedIterator adds a next_back() method, enabling rev() to iterate from the end. This is useful for bidirectional traversal without collecting into a Vec first.

5

Performance: Iterators vs Loops

Rust iterators are a zero-cost abstraction. The compiler optimizes iterator chains into the same machine code as hand-written loops, so you get the expressiveness of functional style with no runtime penalty.

Rust
// Zero-cost abstraction: iterators compile to same code as loops
// Method 1: For loop
let v = vec![1, 2, 3, 4, 5];
let mut sum = 0;
for i in v {
    sum += i;
// Method 2: Iterator
let sum: i32 = v.iter().sum();
// Both produce identical machine code!
// Closure example: both compile to same code
let numbers = vec![1, 2, 3, 4, 5];
// Using iterator adaptor
let result1: Vec<i32> = numbers.iter()
    .map(|x| x * 2)
    .filter(|x| x > &4)
    .collect();
// Using explicit loop
let mut result2 = Vec::new();
for x in &numbers {
    let doubled = x * 2;
    if doubled > 4 {
        result2.push(doubled);
    }
assert_eq!(result1, result2);
Rust
// Example: data processing pipeline
struct User {
    id: u32,
    name: String,
    age: u32,
let users = vec![
    User { id: 1, name: String::from("Alice"), age: 30 },
    User { id: 2, name: String::from("Bob"), age: 25 },
    User { id: 3, name: String::from("Charlie"), age: 35 },
];
// Iterator approach: clear and efficient
let adult_names: Vec<String> = users.iter()
    .filter(|u| u.age > 30)
    .map(|u| u.name.clone())
    .collect();
// Real-world processing pipeline
fn process_data(numbers: Vec<i32>) -> i32 {
    numbers.iter()
        .filter(|x| *x % 2 == 0) // Keep evens
        .map(|x| x * x)             // Square them
        .take(3)                     // Take first 3
        .sum()                       // Sum them
println!("{}", process_data(vec![1, 2, 3, 4, 5, 6])); // 4 + 16 + 36 = 56
Rust
// Lazy evaluation advantage
let v = vec![1, 2, 3, 4, 5];
let iter = v.iter()
    .map(|x| {
        println!("Mapping {}", x);
        x * 2
    });
// Nothing printed yet - iterator is lazy!
let collected: Vec<i32> = iter.collect();
// Now "Mapping 1 2 3 4 5" is printed

Zero-Cost Abstractions

Rust's iterator chains are optimized away at compile time through monomorphization and inlining. The resulting machine code is identical to hand-written loops. You never pay a runtime cost for the functional style.

6

Practical Example: Data Processing Pipeline

Combining closures and iterators enables expressive data processing pipelines. Here is a complete example showing multiple pipeline patterns on a collection of records.

Rust
struct Record {
    id: u32,
    name: String,
    value: f64,
    category: String,
impl Record {
    fn new(id: u32, name: &str, value: f64, category: &str) -> Self {
        Record {
            id,
            name: String::from(name),
            value,
            category: String::from(category),
        }
    }
fn main() {
    let records = vec![
        Record::new(1, "Item A", 100.5, "Electronics"),
        Record::new(2, "Item B", 50.0, "Books"),
        Record::new(3, "Item C", 200.0, "Electronics"),
        Record::new(4, "Item D", 75.5, "Clothes"),
        Record::new(5, "Item E", 150.0, "Electronics"),
    ];
    // Pipeline 1: Get high-value items
    let high_value: Vec<&Record> = records.iter()
        .filter(|r| r.value > 100.0)
        .collect();
    // Pipeline 2: Group by category and sum values
    use std::collections::HashMap;
    let by_category: HashMap<String, f64> = records.iter()
        .fold(HashMap::new(), |mut map, r| {
            *map.entry(r.category.clone()).or_insert(0.0) += r.value;
            map
        });
    // Pipeline 3: Electronics total using filter + map + fold
    let electronics_total: f64 = records.iter()
        .filter(|r| r.category == "Electronics")
        .map(|r| r.value)
        .fold(0.0, |acc, v| acc + v);
    println!("Electronics total: {}", electronics_total);
    // Pipeline 4: Create formatted report
    let report: String = records.iter()
        .enumerate()
        .map(|(idx, r)| format!("{}. {} - ${:.2}", idx + 1, r.name, r.value))
        .collect::<Vec<String>>()
        .join("\n");
    println!("{}", report);
}
💡

Pipeline Best Practices

Use fold for aggregations like grouping, filter + map + collect for transformations, and enumerate when you need indices. The turbofish syntax collect::<Vec<String>>() tells Rust what type to collect into when it cannot be inferred.

← Previous Chapter 8 of 17 Next →