Advanced Functions and Closures in Rust
This chapter explores advanced techniques for working with functions and closures in Rust. While closures are highly flexible and commonly used for inline operations, Rust also allows you to pass regular functions as arguments using function pointers, leverage tuple struct initializers as functions, and return closures from functions using trait objects.
1. The Core Distinction: Function Pointers vs. Closures
In Rust, you can pass closures to functions, but you can also pass regular functions to other functions. This is highly useful when you want to pass a function that has already been defined elsewhere, rather than declaring a new inline closure.
To pass a function as an argument, Rust uses function pointers. The table below highlights the key differences between function pointers and closures:
2. Deep Dive into Function Pointers (fn)
Function pointers are specified using the lowercase fn type. Do not confuse this with the uppercase Fn trait used for closure trait bounds.
Code Example: Passing a Function Pointer
The following example demonstrates a function do_twice that accepts a function pointer f and an integer argument arg. It executes f twice and returns the sum of the results.
fn add_one(x: i32) -> i32 {
x + 1
}
// f is a function pointer of type `fn(i32) -> i32`
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); // Output: 12
}
Execution Flow Walkthrough
3. The Three Closure Traits and Their Relationship to fn
When working with closures, Rust uses three distinct traits to define how variables are captured from the enclosing scope:
Note: Function pointers (
fn) do not capture an environment at all. Because they require the least restrictive environment access, function pointers implement all three closure traits (Fn,FnMut, andFnOnce).
This means that if a function expects a closure trait as an argument, you can pass either a closure or a regular function pointer.
4. Designing Flexible APIs with Trait Bounds
Because function pointers implement the closure traits, it is considered best practice to write functions using generic closure traits rather than concrete function pointers. This makes your API flexible enough to accept both closures and predefined functions.
We can refactor our previous do_twice function to accept any type T that implements the Fn trait:
// Refactored to use a generic trait bound
fn do_twice<T>(f: T, arg: i32) -> i32
where
T: Fn(i32) -> i32,
{
f(arg) + f(arg)
}
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
// We can still pass a regular function pointer
let answer_with_fn = do_twice(add_one, 5);
// We can also pass an inline closure
let answer_with_closure = do_twice(|x| x + 1, 5);
println!("Fn pointer: {}, Closure: {}", answer_with_fn, answer_with_closure);
}
5. When to Restrict APIs to Function Pointers (fn)
While accepting closure traits is generally preferred, there is one major exception: interfacing with external code (Foreign Function Interface / FFI).
Languages like C support passing functions as arguments, but they do not have a concept of closures because they cannot dynamically capture environments. If you are writing a Rust interface to be called by or to call into C code, you must restrict your parameters to the concrete fn function pointer type.
6. Practical Application: Mapping with Function Pointers
A common use case for choosing between closures and function pointers is when mapping over collections.
Option A: Using an Inline Closure
This is the standard approach most developers use to convert a vector of integers into a vector of strings:
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(|i| i.to_string())
.collect();
}
Option B: Using a Function Pointer (Fully Qualified Syntax)
Instead of writing an inline closure, you can pass the to_string function pointer directly. Because multiple traits in scope might define a to_string method, you must use fully qualified syntax:
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(ToString::to_string) // Fully qualified function pointer
.collect();
}
7. Tuple Structs and Enum Variants as Function Pointers
An interesting implementation detail in Rust is that tuple structs and tuple enum variants use parentheses to initialize their values, which syntactically looks like a function call.
Conceptually, these initializers are actually implemented as functions that accept arguments and return an instance of the struct or enum. Consequently, you can use these initializers as function pointers.
Code Example: Mapping with Enum Initializers
#[derive(Debug)]
enum Status {
Value(u32),
Stop,
}
fn main() {
// Status::Value acts as a function pointer of type `fn(u32) -> Status`
let list_of_statuses: Vec<Status> = (0u32..5)
.map(Status::Value)
.collect();
println!("{:?}", list_of_statuses);
// Output: [Value(0), Value(1), Value(2), Value(3), Value(4)]
}
8. Returning Closures and the Single-Type Limitation
Because closures are represented by traits, you cannot specify a concrete closure type as a return type.
The Naive Approach: impl Trait
If your function returns a single closure, you can use the impl Trait syntax:
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
The Problem: Conditional Closures
The impl Trait syntax only works if the function returns a single concrete type. However, in Rust, every closure has a unique, compiler-generated type, even if their signatures and implementations are identical.
If you attempt to return different closures conditionally using impl Trait, the code will fail to compile:
// THIS WILL NOT COMPILE!
fn returns_closure_conditional(a: i32) -> impl Fn(i32) -> i32 {
if a > 0 {
move |b| a + b
} else {
move |b| a - b // Error: expected closure, found a different closure
}
}
The compiler throws an error stating that no two closures have the same type.
9. Solving the Conditional Closure Return with Boxed Trait Objects
To return different closures dynamically, you must use a trait object. Because Rust needs to know the size of the return type at compile time, and closures have variable sizes, you must wrap the trait object in a pointer, such as a Box.
Corrected Code: Using Box<dyn Fn(...)>
fn returns_closure_conditional(a: i32) -> Box<dyn Fn(i32) -> i32> {
if a > 0 {
Box::new(move |b| a + b)
} else {
Box::new(move |b| a - b)
}
}
fn main() {
let add_closure = returns_closure_conditional(5);
let sub_closure = returns_closure_conditional(-5);
println!("Result 1: {}", add_closure(10)); // Output: 15
println!("Result 2: {}", sub_closure(10)); // Output: -15
}
Summary and Key Takeaways
- Function Pointers (
fn): Represented by the lowercasefntype. They point to predefined functions and cannot capture variables from their environment. - Closure Traits (
Fn,FnMut,FnOnce): Represented by uppercase traits that define how closures capture variables from their environment. - Automatic Implementation: Because function pointers do not capture an environment, they automatically implement all three closure traits (
Fn,FnMut, andFnOnce). - API Flexibility: It is best practice to write functions that accept closure traits (
T: Fn(...)) so that callers can pass either closures or regular function pointers. - FFI Integration: When interfacing with external code (like C), you must use the concrete
fnpointer type, as C does not support closures. - Fully Qualified Syntax: You can pass standard library methods (like
ToString::to_string) directly to combinators likemapusing fully qualified path syntax. - Tuple Initializers: Tuple structs and enum variants (like
Status::Value) can be used directly as function pointers because their initializers are implemented as functions. - Returning Closures: You can return a single closure using
impl Fn(...). To return multiple different closures conditionally, you must return a boxed trait object:Box<dyn Fn(...)>.