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 (
fnitems) — declared withfn name(...) { ... }. Every function has its own unique, zero-sized type, but it can be coerced to a function pointer type writtenfn(ArgTypes) -> ReturnTypewhen 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, orFnOnce) 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 sayingFmust be callable with one argument of typeArgType, returningReturnType. SwapFnforFnMutorFnOncedepending 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 implementsFn" 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_twicefor the concrete closure type ofadd_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 bodyx + 3withx = 10, producing13. - The outer call then runs
f(13), producing16, 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
FnoverFnMut, andFnMutoverFnOnce, so your function is usable in the widest range of situations. - Use
movewhenever 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(...) -> Tas a return type when you only ever return one kind of closure from a function; reach forBox<dyn Fn(...) -> T>only when different code paths need to return different closure types. - Reach for the
fnpointer type only when you know you’ll never need to pass a capturing closure — genericFnbounds 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(), aforloop, 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 appliesfto every element of the slice and returns a newVec<i32>. Call it once with a closure that squares each number. - Write a function
make_greeter(greeting: String) -> impl Fn(&str) -> Stringthat returns a closure taking a name and producing a full greeting string (for example, calling it with"Alice"should produce"Hello, Alice!"ifgreetingwas"Hello"). Think carefully about whether the returned closure needsmove. - Given
let words = vec!["rust", "is", "fun", "to", "learn"];, use.iter(),.filter(), and.collect()to build aVec<&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
fnpointer; a capturing closure cannot and needs a genericFn/FnMut/FnOncebound instead. Fnborrows captures immutably and can be called repeatedly;FnMutborrows mutably;FnOnceconsumes a capture and can only be called once.- Use
moveto make a closure own its captured data — essential when returning closures or sending them across threads. - Return closures with
impl Fn(...) -> T(orBox<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 fullIteratorlesson goes much deeper into this later in the course.
