blog.dopana

Back

Fourteenth post in the intermediate Rust series. After mastering smart pointers, we now turn to Rust’s functional programming features. Rust borrows heavily from functional languages: it gives you closures (anonymous functions) and iterators (lazy, composable sequences). The key promise is zero-cost abstractions — these high-level constructs compile down to the same machine code as hand-written loops.

graph TD
    A["Functional Rust"] --> B["Closures<br/>Fn, FnMut, FnOnce"]
    A --> C["Iterators<br/>Lazy evaluation"]
    A --> D["Zero-Cost Abstractions"]
    A --> E["map, filter, fold"]

Closures: Anonymous Functions#

A closure is an anonymous function that can capture its environment. Unlike regular functions, closures can “close over” variables from their surrounding scope.

Basic Syntax#

fn main() {
    let add_one = |x: i32| x + 1;
    println!("Result: {}", add_one(5)); // Result: 6

    // Closures can capture the environment
    let multiplier = 3;
    let multiply = |x: i32| x * multiplier;
    println!("Result: {}", multiply(5)); // Result: 15
}
rust

The |x: i32| syntax defines the parameter list, similar to closures in other languages.

Capturing the Environment#

Closures capture variables from their environment in three ways:

  1. Fn — captures by reference (&T)
  2. FnMut — captures by mutable reference (&mut T)
  3. FnOnce — captures by value (moves ownership)

[!NOTE] A closure that implements FnOnce can also be used as FnMut or Fn, but not vice versa. The compiler infers the least restrictive trait needed.

Closures as Function Arguments#

Closures are most powerful when passed as arguments to higher-order functions:

fn apply_to_list<T>(list: &[T], f: impl Fn(&T)) {
    for item in list {
        f(item);
    }
}

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    apply_to_list(&numbers, |x| println!("Number: {x}"));
}
rust

Closures as Return Values#

You can also return closures from functions using impl Fn:

fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    |x| x * factor
}

fn main() {
    let double = make_multiplier(2);
    let triple = make_multiplier(3);

    println!("Double 5: {}", double(5)); // Double 5: 10
    println!("Triple 5: {}", triple(5)); // Triple 5: 15
}
rust

Iterators: Lazy Sequences#

An iterator is a construct that produces a sequence of values. In Rust, iterators are lazy — they do nothing until you call a consuming method like collect() or for.

Creating Iterators#

Every collection in Rust has an .iter() method that returns an iterator:

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

    // .iter() returns an iterator
    let iter = numbers.iter();

    // Iterators are lazy — nothing happens yet
    for num in iter {
        println!("{num}");
    }
}
rust

Iterator Adapters#

Iterator adapters take an iterator and return a new iterator. The most common ones are map, filter, and fold:

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

    let result: Vec<i32> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)   // keep even numbers
        .map(|&x| x * 2)           // double each
        .collect();                // collect into a Vec

    println!("{:?}", result); // [4, 8, 12, 16, 20]
}
rust
graph LR
    A["[1,2,3,4,5,6,7,8,9,10]"] --> B["filter: even"]
    B --> C["[2,4,6,8,10]"]
    C --> D["map: x2"]
    D --> E["[4,8,12,16,20]"]

The fold Method#

fold is the functional equivalent of a for loop with an accumulator:

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

    // Sum all numbers
    let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("Sum: {sum}"); // Sum: 15

    // Find the maximum
    let max = numbers.iter().fold(0, |acc, &x| if x > acc { x } else { acc });
    println!("Max: {max}"); // Max: 5
}
rust

Custom Iterators with impl Iterator#

You can create your own iterators by implementing the Iterator trait:

Zero-Cost Abstractions#

The hallmark of Rust’s functional features is that they compile down to the same machine code as manual loops. Let’s prove it:

// Functional style
fn functional_sum(numbers: &[i32]) -> i32 {
    numbers.iter().filter(|&&x| x > &0).map(|&x| x * 2).sum()
}

// Imperative style
fn imperative_sum(numbers: &[i32]) -> i32 {
    let mut sum = 0;
    for &x in numbers {
        if x > 0 {
            sum += x * 2;
        }
    }
    sum
}
rust

Both functions produce identical assembly. The compiler optimizes away the iterator chain entirely.

[!TIP] Rust’s iterators are a textbook example of zero-cost abstractions. You get the expressiveness of functional programming with the performance of hand-optimized C.

ELI5 Analogy: The Assembly Line#

Think of iterators like an assembly line in a factory:

  • The raw materials (the original collection) arrive at the start.
  • Each adapter (map, filter, etc.) is a workstation that transforms or removes items.
  • The conveyor belt (the iterator) is lazy — items only move when a worker at the end pulls them.
  • The final consumer (collect, sum, for) is the worker who takes the finished products off the belt.

Because the belt is lazy, if the worker only needs 3 items, the factory only processes 3 — it doesn’t waste energy processing the entire batch!

Summary#

  • Closures are anonymous functions that can capture their environment, with three capture modes: Fn, FnMut, and FnOnce.
  • Iterators are lazy sequences that produce values on demand, with powerful adapters like map, filter, and fold.
  • Zero-cost abstractions mean functional-style code compiles to the same machine code as imperative loops.
  • Custom iterators can be created by implementing the Iterator trait.

In the next post, we will dive into Fearless Concurrency — Rust’s approach to safe parallel programming with threads, channels, and shared state.

References#