Skip to content
Codeloom
Rust

Rust Iterators and Closures: Functional Programming

Master Rust iterators and closures for functional-style programming. Covers map, filter, fold, chaining, custom iterators, and Fn trait family.

·10 min read · By Codeloom
Intermediate 14 min read

What you'll learn

  • Write closures and understand Fn, FnMut, and FnOnce traits
  • Chain iterator adapters for expressive data transformations
  • Build custom iterators with the Iterator trait
  • Use iterators to replace loops with cleaner, faster code

Prerequisites

  • Rust basics — see /blog/rust-variables-and-types
  • Ownership and borrowing — see /blog/rust-ownership-basics
  • Traits — see /blog/rust-traits-basics

Rust’s iterators and closures bring functional programming into a systems language without sacrificing performance. Iterator chains compile down to the same machine code as hand-written loops — often faster because the compiler can optimize across the entire pipeline. Closures capture their environment safely, following Rust’s ownership rules.

This guide walks through closures and iterators together, since you almost always use them in combination.

Closures: Anonymous Functions That Capture

A closure is an anonymous function that can capture variables from its surrounding scope:

fn main() {
    let multiplier = 3;

    // Closure that captures `multiplier`
    let multiply = |x: i32| x * multiplier;

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

Closures infer their parameter and return types from usage, so you rarely need type annotations. But you can add them for clarity:

let add = |a: i32, b: i32| -> i32 { a + b };

How Closures Capture

Rust closures capture variables in three ways, corresponding to three traits:

  • Fn — borrows immutably. The closure can be called multiple times and the captured data stays unchanged.
  • FnMut — borrows mutably. The closure can modify captured data.
  • FnOnce — takes ownership. The closure can only be called once because it consumes the captured data.

The compiler chooses the least restrictive capture automatically:

fn main() {
    // Fn — borrows name immutably
    let name = String::from("Alice");
    let greet = || println!("Hello, {}!", name);
    greet();
    greet(); // Can call multiple times
    println!("{}", name); // name still usable

    // FnMut — borrows count mutably
    let mut count = 0;
    let mut increment = || {
        count += 1;
        count
    };
    println!("{}", increment()); // 1
    println!("{}", increment()); // 2

    // FnOnce — takes ownership of data
    let data = vec![1, 2, 3];
    let consume = move || {
        println!("Consumed: {:?}", data);
        drop(data);
    };
    consume();
    // consume(); // ERROR — cannot call again
    // println!("{:?}", data); // ERROR — data was moved
}

The move keyword forces the closure to take ownership of all captured variables. This is essential when passing closures to threads or returning them from functions.

Closures as Function Parameters

Use trait bounds to accept closures:

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

fn apply_and_collect<F: FnMut(i32) -> i32>(mut f: F, items: &[i32]) -> Vec<i32> {
    items.iter().map(|&x| f(x)).collect()
}

fn main() {
    let result = apply_twice(|x| x + 3, 5);
    println!("{}", result); // 11

    let mut offset = 0;
    let results = apply_and_collect(|x| { offset += 1; x + offset }, &[10, 20, 30]);
    println!("{:?}", results); // [11, 22, 33]
}

Returning Closures

To return a closure from a function, use impl Fn:

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

fn make_greeting(prefix: String) -> impl Fn(&str) -> String {
    move |name| format!("{}, {}!", prefix, name)
}

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

    let hello = make_greeting("Hello".to_string());
    println!("{}", hello("World")); // "Hello, World!"
}

Iterators: Lazy Data Pipelines

An iterator is any type that implements the Iterator trait, which requires one method:

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

Iterators are lazy — they produce values one at a time and do no work until consumed.

Creating Iterators

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

    // .iter() borrows each element
    for n in numbers.iter() {
        println!("{}", n); // n is &i32
    }

    // .iter_mut() borrows mutably
    let mut numbers = vec![1, 2, 3, 4, 5];
    for n in numbers.iter_mut() {
        *n *= 2;
    }
    println!("{:?}", numbers); // [2, 4, 6, 8, 10]

    // .into_iter() takes ownership
    let numbers = vec![1, 2, 3];
    for n in numbers.into_iter() {
        println!("{}", n); // n is i32
    }
    // numbers is consumed — cannot use it here
}

Iterator Adapters

Adapters transform iterators into new iterators. They are lazy — nothing happens until you consume the result.

map — Transform Each Element

fn main() {
    let names = vec!["alice", "bob", "charlie"];
    let capitalized: Vec<String> = names
        .iter()
        .map(|name| {
            let mut chars = name.chars();
            match chars.next() {
                None => String::new(),
                Some(c) => c.to_uppercase().to_string() + chars.as_str(),
            }
        })
        .collect();

    println!("{:?}", capitalized); // ["Alice", "Bob", "Charlie"]
}

filter — Keep Elements That Match

fn main() {
    let numbers: Vec<i32> = (1..=20).collect();

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

    println!("{:?}", even_numbers); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
}

filter_map — Filter and Transform in One Step

fn main() {
    let strings = vec!["42", "hello", "93", "world", "7"];

    let numbers: Vec<i32> = strings
        .iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();

    println!("{:?}", numbers); // [42, 93, 7]
}

filter_map is cleaner than .map().filter().map() when you want to both check and transform.

flat_map — Flatten Nested Iterators

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

    let words: Vec<&str> = sentences
        .iter()
        .flat_map(|s| s.split_whitespace())
        .collect();

    println!("{:?}", words); // ["hello", "world", "foo", "bar", "baz"]
}

enumerate, zip, and chain

fn main() {
    let fruits = vec!["apple", "banana", "cherry"];

    // enumerate adds indices
    for (i, fruit) in fruits.iter().enumerate() {
        println!("{}: {}", i, fruit);
    }

    // zip pairs elements from two iterators
    let prices = vec![1.50, 0.75, 2.00];
    let menu: Vec<(&str, f64)> = fruits.iter()
        .copied()
        .zip(prices.iter().copied())
        .collect();
    println!("{:?}", menu);

    // chain concatenates iterators
    let more_fruits = vec!["date", "elderberry"];
    let all: Vec<&&str> = fruits.iter().chain(more_fruits.iter()).collect();
    println!("{:?}", all);
}

take, skip, and windows

fn main() {
    let numbers: Vec<i32> = (1..=100).collect();

    // First 5
    let first_five: Vec<&i32> = numbers.iter().take(5).collect();
    println!("{:?}", first_five); // [1, 2, 3, 4, 5]

    // Skip first 95
    let last_five: Vec<&i32> = numbers.iter().skip(95).collect();
    println!("{:?}", last_five); // [96, 97, 98, 99, 100]

    // Sliding windows
    let data = vec![1, 3, 5, 7, 9];
    let averages: Vec<f64> = data
        .windows(3)
        .map(|w| w.iter().sum::<i32>() as f64 / w.len() as f64)
        .collect();
    println!("{:?}", averages); // [3.0, 5.0, 7.0]
}

Consuming Iterators

Consumers drive the iterator to completion and produce a final value.

collect — Gather into a Collection

use std::collections::HashMap;

fn main() {
    // Into a Vec
    let squares: Vec<i32> = (1..=5).map(|n| n * n).collect();
    println!("{:?}", squares); // [1, 4, 9, 16, 25]

    // Into a HashMap
    let entries = vec![("name", "Alice"), ("role", "admin")];
    let map: HashMap<&str, &str> = entries.into_iter().collect();
    println!("{:?}", map);

    // Into a String
    let greeting: String = vec!['H', 'e', 'l', 'l', 'o'].into_iter().collect();
    println!("{}", greeting);
}

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);
    println!("Sum: {}", sum); // 15

    let product = numbers.iter().fold(1, |acc, &n| acc * n);
    println!("Product: {}", product); // 120

    // Build a string with fold
    let csv = numbers
        .iter()
        .fold(String::new(), |mut acc, n| {
            if !acc.is_empty() {
                acc.push(',');
            }
            acc.push_str(&n.to_string());
            acc
        });
    println!("CSV: {}", csv); // "1,2,3,4,5"
}

find, any, all, position

fn main() {
    let numbers = vec![10, 20, 35, 40, 55];

    let first_odd = numbers.iter().find(|&&n| n % 2 != 0);
    println!("First odd: {:?}", first_odd); // Some(35)

    let has_even = numbers.iter().any(|&n| n % 2 == 0);
    println!("Has even: {}", has_even); // true

    let all_positive = numbers.iter().all(|&n| n > 0);
    println!("All positive: {}", all_positive); // true

    let pos = numbers.iter().position(|&n| n > 30);
    println!("First > 30 at index: {:?}", pos); // Some(2)
}

Chaining It All Together

The real power comes from composing adapters into pipelines:

#[derive(Debug)]
struct LogEntry {
    level: String,
    message: String,
    timestamp: u64,
}

fn analyze_logs(logs: &[LogEntry]) {
    // Count errors
    let error_count = logs.iter()
        .filter(|log| log.level == "ERROR")
        .count();
    println!("Errors: {}", error_count);

    // Get unique error messages sorted
    let mut error_messages: Vec<&str> = logs.iter()
        .filter(|log| log.level == "ERROR")
        .map(|log| log.message.as_str())
        .collect();
    error_messages.sort();
    error_messages.dedup();
    println!("Unique errors: {:?}", error_messages);

    // Most recent 5 warnings
    let recent_warnings: Vec<&str> = logs.iter()
        .filter(|log| log.level == "WARN")
        .map(|log| log.message.as_str())
        .rev()
        .take(5)
        .collect();
    println!("Recent warnings: {:?}", recent_warnings);

    // Group counts by level
    let mut counts = std::collections::HashMap::new();
    for log in logs {
        *counts.entry(&log.level).or_insert(0) += 1;
    }
    println!("Counts: {:?}", counts);
}

Building a Custom Iterator

Implement the Iterator trait on your own types:

struct Fibonacci {
    a: u64,
    b: u64,
}

impl Fibonacci {
    fn new() -> Self {
        Fibonacci { a: 0, b: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let value = self.a;
        let next = self.a.checked_add(self.b)?; // Returns None on overflow
        self.a = self.b;
        self.b = next;
        Some(value)
    }
}

fn main() {
    // First 10 Fibonacci numbers
    let fibs: Vec<u64> = Fibonacci::new().take(10).collect();
    println!("{:?}", fibs); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    // Sum of Fibonacci numbers below 1000
    let sum: u64 = Fibonacci::new()
        .take_while(|&n| n < 1000)
        .sum();
    println!("Sum below 1000: {}", sum);

    // Even Fibonacci numbers below 1000
    let even_fibs: Vec<u64> = Fibonacci::new()
        .take_while(|&n| n < 1000)
        .filter(|n| n % 2 == 0)
        .collect();
    println!("Even fibs: {:?}", even_fibs);
}

Iterator for a Custom Collection

struct Matrix {
    data: Vec<Vec<f64>>,
    rows: usize,
    cols: usize,
}

struct MatrixIter<'a> {
    matrix: &'a Matrix,
    row: usize,
    col: usize,
}

impl Matrix {
    fn new(data: Vec<Vec<f64>>) -> Self {
        let rows = data.len();
        let cols = if rows > 0 { data[0].len() } else { 0 };
        Matrix { data, rows, cols }
    }

    fn iter(&self) -> MatrixIter<'_> {
        MatrixIter {
            matrix: self,
            row: 0,
            col: 0,
        }
    }
}

impl<'a> Iterator for MatrixIter<'a> {
    type Item = (usize, usize, f64);

    fn next(&mut self) -> Option<Self::Item> {
        if self.row >= self.matrix.rows {
            return None;
        }
        let value = self.matrix.data[self.row][self.col];
        let result = (self.row, self.col, value);

        self.col += 1;
        if self.col >= self.matrix.cols {
            self.col = 0;
            self.row += 1;
        }

        Some(result)
    }
}

fn main() {
    let matrix = Matrix::new(vec![
        vec![1.0, 2.0, 3.0],
        vec![4.0, 5.0, 6.0],
    ]);

    for (r, c, val) in matrix.iter() {
        println!("[{},{}] = {}", r, c, val);
    }

    let sum: f64 = matrix.iter().map(|(_, _, v)| v).sum();
    println!("Sum: {}", sum); // 21.0
}

Performance: Iterators vs Loops

Iterator chains and for loops compile to the same assembly in most cases. Rust’s zero-cost abstractions mean the compiler inlines closures and eliminates iterator overhead. In benchmarks, iterator chains sometimes outperform hand-written loops because the compiler can apply SIMD optimizations more easily.

// These produce identical assembly:
fn sum_loop(data: &[i32]) -> i32 {
    let mut sum = 0;
    for &n in data {
        sum += n;
    }
    sum
}

fn sum_iter(data: &[i32]) -> i32 {
    data.iter().sum()
}

Use whichever style is clearer for your specific case. Iterators tend to be cleaner for transformation pipelines; loops can be clearer for complex stateful operations.

Wrapping Up

Closures and iterators are the foundation of idiomatic Rust. The core ideas are:

  • Closures capture variables following ownership rules. Use Fn, FnMut, or FnOnce bounds depending on how the closure uses captured data.
  • Iterators are lazy — adapters like map, filter, and flat_map build a pipeline that runs only when consumed.
  • Consumers like collect, fold, sum, any, and find drive the pipeline to produce results.
  • Custom iterators implement one method: next(&mut self) -> Option<Self::Item>.
  • There is no runtime cost — iterator chains compile to the same efficient code as hand-written loops.

Once iterator chains become natural to you, you will find yourself reaching for them constantly. They make data transformations concise, composable, and safe.