Higher-Order Functions and Iterators Preview

A higher-order function is simply a function that takes another function (or closure) as a parameter, or returns one as its result. Rust treats functions and closures as first-class values, which means you can store them in variables, pass them around, and return them from other functions just like any integer or string. This single idea is the foundation of Rust’s iterator system — the powerful chain of .map(), .filter(), and .collect() calls you will see throughout idiomatic Rust code all rest on higher-order functions.

Overview / How It Works

In Rust there are three closely related kinds of "callable" values, and understanding the differences up front will save you a lot of confusion later:

  • Plain functions (fn items) — declared with fn name(...) { ... }. Every function has its own unique, zero-sized type, but it can be coerced to a function pointer type written fn(ArgTypes) -> ReturnType when you need to store it in a variable or pass it around.
  • Closures — anonymous functions written with pipes, like |x| x + 1, that can capture variables from the surrounding scope. A closure is compiled into a unique, compiler-generated struct that holds whatever it captured, plus a call method.
  • Trait objects / generics over closures — because every closure has its own unique type, generic code that accepts "some closure" is written with a trait bound (Fn, FnMut, or FnOnce) rather than a single concrete type.

The key mental model: a closure that captures nothing from its environment behaves just like a plain function and can be used as a fn pointer. A closure that does capture something (a variable from an outer scope) carries that captured data with it, and can no longer be coerced to a bare fn pointer — it needs a generic parameter bounded by one of the three closure traits instead.

The three closure traits

Rust distinguishes closures by how they use what they capture. The compiler infers this automatically from the closure body:

Trait What it means Can be called
Fn Borrows captured variables immutably (or captures nothing) Any number of times
FnMut Borrows captured variables mutably Any number of times, but needs a mutable binding
FnOnce Consumes (moves) a captured variable when called Exactly once

Every closure implements at least FnOnce. Closures that don’t need to consume their captures also implement FnMut, and closures that don’t need to mutate anything also implement Fn. Because Fn is the most restrictive (and most reusable) of the three, function signatures typically ask for the least powerful trait that gets the job done — usually Fn, unless the closure genuinely needs to mutate state or consume ownership.

Returning a closure from a function introduces one more wrinkle: the closure’s concrete type is anonymous and compiler-generated, so you can’t write it out by hand. Instead you return impl Fn(ArgTypes) -> ReturnType (an opaque type that implements the trait) or, if you need multiple different closure types to be returned from the same function, you box it as Box<dyn Fn(ArgTypes) -> ReturnType>. This lesson previews iterators for the same reason: methods like .map() and .filter() are themselves higher-order functions — they take a closure argument and apply it lazily to each element as the iterator is consumed.

Syntax

The general shape of a function that accepts a closure looks like this:

fn function_name<F>(param: F) -> ReturnType
where
    F: Fn(ArgType) -> ReturnType,
{
    // call param(...) somewhere in the body
}
  • F — a generic type parameter standing in for "whatever concrete closure type gets passed in".
  • F: Fn(ArgType) -> ReturnType — the trait bound saying F must be callable with one argument of type ArgType, returning ReturnType. Swap Fn for FnMut or FnOnce depending on what the closure needs to do with its captures.
  • fn(ArgType) -> ReturnType (lowercase, no generic) — a concrete function pointer type. Use this only when you know you’ll never need to pass a capturing closure.
  • impl Fn(ArgType) -> ReturnType — used as a return type to hand back "some closure that implements Fn" without naming its type.

Examples

Example 1: Passing a plain function by pointer

fn apply(f: fn(i32) -> i32, value: i32) -> i32 {
    f(value)
}

fn square(x: i32) -> i32 {
    x * x
}

fn double(x: i32) -> i32 {
    x * 2
}

fn main() {
    println!("{}", apply(square, 5));
    println!("{}", apply(double, 5));
}

Output:

25
10

Because square and double capture nothing from their environment, they coerce directly to the fn(i32) -> i32 pointer type that apply expects. apply is a higher-order function: it receives a function as data and calls it later.

Example 2: A generic function accepting any closure

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

fn main() {
    let add_three = |x| x + 3;
    let result = apply_twice(add_three, 10);
    println!("{}", result);
}

Output:

16

Unlike apply in Example 1, apply_twice takes a generic F: Fn(i32) -> i32, so it accepts any callable with that signature — a bare function, or, as shown here, a closure. add_three captures no environment either, but it is still a closure value, not a function pointer, and only the trait-bounded version of apply would accept a closure that does capture something.

Example 3: Returning a closure that captures its environment

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

fn main() {
    let times_three = make_multiplier(3);
    println!("{}", times_three(7));
    println!("{}", times_three(10));
}

Output:

21
30

make_multiplier returns a closure that remembers factor long after make_multiplier itself has returned. The move keyword forces the closure to take ownership of factor (an i32, so this is a cheap copy) instead of borrowing it, which is required here — a borrowed reference to factor would become invalid the instant make_multiplier‘s stack frame is gone.

Example 4: A first look at iterator adapters

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

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

    println!("{:?}", doubled_evens);

    let sum: i32 = numbers.iter().sum();
    println!("Sum: {}", sum);
}

Output:

[4, 8, 12]
Sum: 21

.filter() and .map() are higher-order functions living on the Iterator trait: each one takes a closure and applies it as elements flow through the chain. numbers.iter() yields &i32 references, so filter‘s closure receives &&i32 (a reference to the iterator item) — the pattern |&&n| destructures both layers down to a plain i32. .collect() is what finally forces the chain to run, gathering results into the Vec<i32> we asked for via the type annotation. This is only a preview — the full Iterator trait, with dozens of adapters, gets its own dedicated lesson later in this course.

How It Works Step by Step

Trace through apply_twice(add_three, 10) from Example 2:

  • The compiler monomorphizes apply_twice for the concrete closure type of add_three — at compile time it generates a specialized version of the function for that exact closure, so there is no runtime overhead or dynamic dispatch involved (this is "static dispatch").
  • Inside the function body, f(f(value)) first evaluates the inner call: f(10) runs the closure body x + 3 with x = 10, producing 13.
  • The outer call then runs f(13), producing 16, which is returned.

Now trace the iterator chain from Example 4. Iterators in Rust are lazy: calling .filter(...) or .map(...) does not loop over anything by itself — it just wraps the previous iterator in a new iterator struct that remembers the closure. Nothing actually runs until a consuming method like .collect() or .sum() pulls values through the whole chain one at a time: collect asks the chain for its first item, which asks map for an item, which asks filter for an item, which asks .iter() for the underlying 1. filter rejects 1 (odd) and keeps pulling until it gets 2, which passes; map then doubles it to 4, and collect pushes 4 into the output Vec. This repeats element by element until the source is exhausted, giving [4, 8, 12].

Common Mistakes

Mistake 1: Returning a closure that borrows a local variable instead of owning it

fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    |y| x + y
}
error[E0373]: closure may outlive the current function, but it borrows `x`, which is owned by the current function

Without move, the closure captures x by reference by default. But x is a local parameter owned by make_adder, and it is dropped the moment make_adder returns — so a closure holding a reference to it would be a dangling pointer, which the borrow checker refuses to allow. The fix is to force the closure to take ownership of x with move:

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

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

Output:

15

Because i32 implements Copy, moving it into the closure is just as cheap as borrowing it would have been — there’s no reason not to use move here. A good rule of thumb: any closure you return from a function should almost always be a move closure, since the function’s local variables won’t outlive the function call.

Mistake 2: Passing a capturing closure where a bare function pointer is expected

fn apply(f: fn(i32) -> i32, value: i32) -> i32 {
    f(value)
}

fn main() {
    let factor = 3;
    let multiply = |x| x * factor;
    println!("{}", apply(multiply, 5));
}
error[E0308]: mismatched types
expected fn pointer `fn(i32) -> i32`
           found closure

multiply captures factor from its environment, so it is a genuine closure with data attached — it cannot be coerced to the zero-capture fn pointer type that apply demands. The fix is to make apply generic over the Fn trait, which accepts both bare functions and capturing closures:

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

fn main() {
    let factor = 3;
    let multiply = |x| x * factor;
    println!("{}", apply(multiply, 5));
}

Output:

15

Unless you specifically need the smaller, non-capturing guarantee of a fn pointer (for example, storing it in a static, or handing it to C code via FFI), prefer writing APIs generic over Fn/FnMut/FnOnce so callers can pass whichever kind of closure fits their needs.

Best Practices

  • Default to accepting the least powerful trait that works: prefer Fn over FnMut, and FnMut over FnOnce, so your function is usable in the widest range of situations.
  • Use move whenever a closure is returned from a function or sent to another thread — it guarantees the closure owns everything it needs instead of holding a borrow that might not outlive its use.
  • Prefer impl Fn(...) -> T as a return type when you only ever return one kind of closure from a function; reach for Box<dyn Fn(...) -> T> only when different code paths need to return different closure types.
  • Reach for the fn pointer type only when you know you’ll never need to pass a capturing closure — generic Fn bounds are more flexible and just as fast, thanks to monomorphization.
  • Remember iterator adapters like .map() and .filter() are lazy: chains do nothing until a consuming method (.collect(), .sum(), .for_each(), a for loop, and so on) is called.
  • Name closures for what they do (is_even, to_uppercase) rather than leaving anonymous inline closures in long iterator chains — it keeps chains readable.

Practice Exercises

  • Write a function transform_all(values: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32> that applies f to every element of the slice and returns a new Vec<i32>. Call it once with a closure that squares each number.
  • Write a function make_greeter(greeting: String) -> impl Fn(&str) -> String that returns a closure taking a name and producing a full greeting string (for example, calling it with "Alice" should produce "Hello, Alice!" if greeting was "Hello"). Think carefully about whether the returned closure needs move.
  • Given let words = vec!["rust", "is", "fun", "to", "learn"];, use .iter(), .filter(), and .collect() to build a Vec<&str> containing only the words with more than two characters. Expected output: ["rust", "fun", "learn"].

Summary

  • Functions and closures are first-class values in Rust — they can be stored, passed as arguments, and returned from other functions.
  • A non-capturing closure (or a plain function) can be used as a fn pointer; a capturing closure cannot and needs a generic Fn/FnMut/FnOnce bound instead.
  • Fn borrows captures immutably and can be called repeatedly; FnMut borrows mutably; FnOnce consumes a capture and can only be called once.
  • Use move to make a closure own its captured data — essential when returning closures or sending them across threads.
  • Return closures with impl Fn(...) -> T (or Box<dyn Fn(...) -> T> for multiple possible closure types), since a closure’s concrete type cannot be written by hand.
  • Iterator adapters like .map() and .filter() are higher-order functions that build a lazy chain, which only runs when a consuming method like .collect() or .sum() is called — a full Iterator lesson goes much deeper into this later in the course.