Skip to main content

Advanced Functions and Closures in Rust

Published: October 29, 2025 Updated: August 28, 2026 Larry Qu 7 min read

Rust’s function and closure system goes far beyond simple definitions. Functions are first-class values with their own types. Closures implement traits that precisely describe how they interact with their environment. Mastering these concepts unlocks powerful API design patterns and functional programming techniques.

Function Pointers (fn)

Functions in Rust have a concrete type: fn(ArgTypes) -> ReturnType. This is the function pointer type (lowercase fn). It refers to a specific function, not a closure, and captures nothing from its environment:

fn add_one(x: i32) -> i32 { x + 1 }
fn subtract_one(x: i32) -> i32 { x - 1 }
fn double(x: i32) -> i32 { x * 2 }

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

fn main() {
    println!("{}", apply(add_one, 5));     // 6
    println!("{}", apply(subtract_one, 5)); // 4
    println!("{}", apply(double, 5));       // 10

    // Store in array/vec
    let operations: Vec<fn(i32) -> i32> = vec![add_one, double, subtract_one];
    let result = operations.iter().fold(10, |acc, f| f(acc));
    println!("{}", result); // ((10 + 1) * 2) - 1 = 21

    // Pass named function where closure expected — fn satisfies Fn + FnMut + FnOnce
    let doubled: Vec<i32> = vec![1, 2, 3].into_iter().map(double).collect();
    println!("{:?}", doubled); // [2, 4, 6]
}

Function pointers are useful for:

  • C FFI (C callbacks expect function pointers, not closures)
  • Storing in const/static (closures can’t be const)
  • When you need the simplicity of no capture and no allocation

The Fn Trait Hierarchy

Every closure and function implements one or more of three traits. They form a hierarchy:

FnOnce  ←  FnMut  ←  Fn
(least strict)       (most strict)
  • FnOnce: can be called at least once; may consume captured values
  • FnMut: can be called many times; may mutate captured values
  • Fn: can be called many times concurrently; only immutably borrows

All Fn closures are also FnMut and FnOnce. All FnMut closures are also FnOnce.

fn call_once<F: FnOnce() -> String>(f: F) -> String { f() }
fn call_mut<F: FnMut() -> String>(mut f: F) -> String { f() }
fn call_ref<F: Fn() -> String>(f: F) -> String { f() }

fn main() {
    let name = String::from("Rust");

    // Fn — immutable borrow
    let greet = || format!("Hello, {}!", name);
    println!("{}", call_ref(&greet));  // Can call ref version
    println!("{}", call_mut(greet));   // And mut version
    // println!("{}", call_once(greet)); // And once version (already moved to call_mut)

    // FnMut — mutable borrow
    let mut count = 0;
    let mut counter = || { count += 1; count };
    println!("{}", call_mut(&mut counter)); // 1
    println!("{}", call_mut(&mut counter)); // 2
    // call_ref(&counter); // Error: requires Fn, but counter is FnMut

    // FnOnce — takes ownership
    let data = vec![1, 2, 3];
    let consume = move || { let sum: i32 = data.iter().sum(); sum };
    println!("{}", call_once(consume)); // 6
    // call_once(consume);  // Error: already consumed
}

Choosing the Right Bound

When you accept a closure, use the least restrictive bound your code needs:

// Accept Fn when you call it multiple times and only need immutable access
fn retry<F: Fn() -> Result<(), String>>(f: F, times: u32) {
    for i in 0..times {
        match f() {
            Ok(()) => return,
            Err(e) => eprintln!("Attempt {}: {}", i + 1, e),
        }
    }
}

// Accept FnMut when you call it multiple times but need mutation
fn run_n_times<F: FnMut()>(mut f: F, n: usize) {
    for _ in 0..n { f(); }
}

// Accept FnOnce when you call it exactly once (most permissive for caller)
fn run_once<F: FnOnce() -> String>(f: F) -> String {
    f()
}

Higher-Order Functions

Functions that take or return other functions:

fn compose<A, B, C>(f: impl Fn(A) -> B, g: impl Fn(B) -> C) -> impl Fn(A) -> C {
    move |x| g(f(x))
}

fn main() {
    let trim_and_upper = compose(
        |s: &str| s.trim().to_string(),
        |s: String| s.to_uppercase(),
    );

    println!("{}", trim_and_upper("  hello world  ")); // HELLO WORLD

    // Building a pipeline
    let parse_and_double = compose(
        |s: &str| s.parse::<i32>().unwrap_or(0),
        |n: i32| n * 2,
    );

    println!("{}", parse_and_double("21")); // 42
}

Currying

fn add(a: i32) -> impl Fn(i32) -> i32 {
    move |b| a + b
}

fn multiply(a: i32) -> impl Fn(i32) -> i32 {
    move |b| a * b
}

fn main() {
    let add5 = add(5);
    let triple = multiply(3);

    println!("{}", add5(10));       // 15
    println!("{}", triple(7));      // 21

    let numbers = vec![1, 2, 3, 4, 5];
    let result: Vec<i32> = numbers.iter().map(|&x| add5(triple(x))).collect();
    println!("{:?}", result); // [8, 11, 14, 17, 20]

    // Apply multiple transforms
    let transforms: Vec<Box<dyn Fn(i32) -> i32>> = vec![
        Box::new(add(10)),
        Box::new(multiply(2)),
        Box::new(add(-5)),
    ];

    let value = transforms.iter().fold(1, |acc, f| f(acc));
    println!("{}", value); // ((1 + 10) * 2) - 5 = 17
}

Returning Closures from Functions

Closures have anonymous types the compiler generates internally — you can’t name them. Two options for returning closures:

Option 1: impl Fn(...) — Zero Cost

The concrete type is fixed at compile time. Only one possible return type:

fn make_greeter(greeting: &str) -> impl Fn(&str) -> String + '_ {
    move |name| format!("{}, {}!", greeting, name)
}

fn make_between_checker(low: i32, high: i32) -> impl Fn(i32) -> bool {
    move |x| x >= low && x <= high
}

fn main() {
    let hello = make_greeter("Hello");
    println!("{}", hello("Alice")); // Hello, Alice!
    println!("{}", hello("Bob"));   // Hello, Bob!

    let is_teen = make_between_checker(13, 19);
    println!("{}", is_teen(15)); // true
    println!("{}", is_teen(25)); // false
}

Option 2: Box<dyn Fn(...)> — When Type Must Be Erased

Use when you have multiple branches returning different closures:

enum Strategy { Aggressive, Conservative }

fn make_strategy(s: Strategy) -> Box<dyn Fn(i32) -> i32> {
    match s {
        Strategy::Aggressive   => Box::new(|x| x * 3),
        Strategy::Conservative => Box::new(|x| x + 1),
    }
}

fn main() {
    let strategies = vec![Strategy::Aggressive, Strategy::Conservative];
    for strategy in strategies {
        let f = make_strategy(strategy);
        println!("{}", f(10)); // 30, then 11
    }
}

Closures as Callbacks

A common pattern in event-driven and asynchronous code:

struct EventBus {
    handlers: Vec<Box<dyn Fn(&str)>>,
}

impl EventBus {
    fn new() -> Self { EventBus { handlers: Vec::new() } }

    fn on<F: Fn(&str) + 'static>(&mut self, handler: F) {
        self.handlers.push(Box::new(handler));
    }

    fn emit(&self, event: &str) {
        for handler in &self.handlers {
            handler(event);
        }
    }
}

fn main() {
    let mut bus = EventBus::new();

    bus.on(|e| println!("[Logger] Event: {}", e));

    let prefix = "ALERT";
    bus.on(move |e| println!("[{}] {}", prefix, e));

    let mut count = 0;
    // Note: for mutable capture, need Mutex or Cell for shared state in Fn
    bus.on(|e| println!("[Counter] Processing: {}", e));

    bus.emit("user.login");
    bus.emit("order.placed");
}

Function Composition Pipeline

A type-safe pipeline builder using closures:

struct Pipeline<T> {
    value: T,
}

impl<T> Pipeline<T> {
    fn new(value: T) -> Self { Pipeline { value } }

    fn pipe<U>(self, f: impl FnOnce(T) -> U) -> Pipeline<U> {
        Pipeline { value: f(self.value) }
    }

    fn result(self) -> T { self.value }
}

fn main() {
    let result = Pipeline::new("  hello, rust!  ")
        .pipe(|s| s.trim().to_string())
        .pipe(|s| s.split(", ").map(String::from).collect::<Vec<_>>())
        .pipe(|words| words.iter().map(|w| {
            let mut chars = w.chars();
            match chars.next() {
                None => String::new(),
                Some(first) => first.to_uppercase().to_string() + chars.as_str(),
            }
        }).collect::<Vec<_>>())
        .pipe(|words| words.join(" "))
        .result();

    println!("{}", result); // Hello Rust!
}

Practical API Design: The Builder Pattern with Closures

Closures enable ergonomic builder APIs:

struct RequestBuilder {
    url: String,
    method: String,
    transform: Box<dyn Fn(String) -> String>,
}

impl RequestBuilder {
    fn new(url: &str) -> Self {
        RequestBuilder {
            url: url.to_string(),
            method: "GET".to_string(),
            transform: Box::new(|s| s),
        }
    }

    fn method(mut self, method: &str) -> Self {
        self.method = method.to_string();
        self
    }

    fn with_transform<F: Fn(String) -> String + 'static>(mut self, f: F) -> Self {
        self.transform = Box::new(f);
        self
    }

    fn build(self) -> String {
        let base = format!("{} {}", self.method, self.url);
        (self.transform)(base)
    }
}

fn main() {
    let request = RequestBuilder::new("/api/users")
        .method("POST")
        .with_transform(|s| s.to_uppercase())
        .build();

    println!("{}", request); // POST /API/USERS
}

fn vs impl Fn vs Box<dyn Fn> — Decision Guide

fn(T) -> U impl Fn(T) -> U Box<dyn Fn(T) -> U>
Captures environment No Yes Yes
const/static Yes No No
Runtime cost Zero Zero Heap + vtable
Multiple return types No No Yes
FFI compatible Yes No No
Stored in struct Yes No (opaque) Yes (explicit)
// Use fn: stateless, const, FFI
const DOUBLE: fn(i32) -> i32 = |x| x * 2;

// Use impl Fn: most function parameters and return types
fn transform(data: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32> {
    data.iter().map(|&x| f(x)).collect()
}

// Use Box<dyn Fn>: stored in structs, heterogeneous collections, conditional returns
struct Handler { f: Box<dyn Fn(i32) -> String> }

Summary

  • fn(T) -> U is a function pointer — no captures, zero size, FFI-compatible
  • Fn, FnMut, FnOnce describe how closures interact with captured state
  • Use the least restrictive bound (Fn over FnMut over FnOnce) in function signatures
  • Return impl Fn(...) for zero-cost static dispatch; Box<dyn Fn(...)> when type erasure is needed
  • Function pointers implement all three Fn traits — they can be used anywhere a closure is expected
  • Higher-order functions, composition, and currying are natural in Rust’s type system

Resources

Comments

👍 Was this article helpful?