Advanced Functions and Closures in Rust

Functions and closures are fundamental building blocks in Rust, but their capabilities extend far beyond simple definitions and calls. Rust’s type system provides powerful, fine-grained control over how functions and closures behave, especially when they are passed as arguments or returned from other functions.

This article explores some of these advanced features, including function pointers and the Fn traits that govern closure behavior.

1. Function Pointers (fn)

In Rust, functions have their own type. The type fn, written with a lowercase ‘f’, is a function pointer. It points to a function that does not capture any variables from its environment. This makes fn a simple, zero-sized type that is useful for interfacing with external code (like C libraries) or for when you need a simple, guaranteed-stateless callable.

You can use fn to pass a regular function to another function.

fn add_one(x: i32) -> i32 {
    x + 1
}

// This function takes a function pointer `f` as an argument.
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);
    println!("The answer is: {}", answer); // Prints 12
}

While you can also pass closures that don’t capture their environment to a function expecting a fn, function pointers are distinct from closures. Closures are more flexible and can capture their environment, which is handled by the Fn traits.

2. The Fn Traits: How Closures Capture Their Environment

Every closure in Rust implicitly implements one or more of three special traits: Fn, FnMut, and FnOnce. The trait that a closure implements is determined by how it uses the variables it captures from its environment. This is one of the most powerful features of Rust’s closure design.

The traits are hierarchical:

  • Any closure that implements Fn also implements FnMut and FnOnce.
  • Any closure that implements FnMut also implements FnOnce.

FnOnce

This trait applies to closures that can be called only once. All closures implement FnOnce because, at a minimum, they can be called one time. A closure that moves a captured value out of its body will only implement FnOnce.

fn main() {
    let s = String::from("hello");

    // This closure takes ownership of `s` and moves it.
    let consume_s = || {
        // `s` is moved into `println!`, so this closure can only be called once.
        println!("{}", s);
    };

    consume_s();
    // consume_s(); // This would not compile.
}

FnMut

This trait is for closures that can mutate the variables they capture. It can be called multiple times.

fn main() {
    let mut s = String::from("hello");

    // This closure takes a mutable borrow of `s`.
    let mut change_s = || {
        s.push_str(", world");
    };

    change_s();
    change_s();

    println!("{}", s); // Prints "hello, world, world"
}

Fn

This trait is for closures that only immutably borrow values from their environment. They can be called multiple times, even concurrently.

fn main() {
    let s = String::from("hello");

    // This closure takes an immutable borrow of `s`.
    let borrow_s = || {
        println!("{}", s);
    };

    borrow_s();
    borrow_s();
}

When writing a function that accepts a closure, using the most generic trait possible (Fn, FnMut, or FnOnce) gives the caller the most flexibility.

3. Returning Closures

Returning a closure from a function is more complex than passing one in. This is because closures are “unsized” types—the compiler doesn’t know the size of a closure at compile time, as it depends on the data it captures.

Because of this, you cannot return a closure directly. Instead, you must return it behind a pointer, typically a Box. You use a trait object to specify the closure’s signature.

// This function returns a closure that takes an i32 and returns an i32.
// We use `Box<dyn Fn(i32) -> i32>` to return the closure on the heap.
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

fn main() {
    let closure = returns_closure();
    let result = closure(5);
    println!("Result: {}", result); // Prints 6
}

The move keyword is often used when returning closures to ensure that any captured data is moved into the closure and has a valid lifetime.

fn returns_closure_with_move(a: i32) -> Box<dyn Fn(i32) -> i32> {
    // The `move` keyword forces the closure to take ownership of `a`.
    Box::new(move |x| x + a)
}

fn main() {
    let closure = returns_closure_with_move(10);
    let result = closure(5);
    println!("Result: {}", result); // Prints 15
}

Conclusion

Rust’s approach to functions and closures provides a remarkable degree of control and safety. By understanding the difference between function pointers (fn) and closures, the hierarchy of the Fn traits, and the techniques for returning closures, you can write highly flexible and efficient APIs that leverage the full power of functional programming patterns in Rust.