Closures and Function Pointers
Rust knows two kinds of first-class functions:
- Function pointers: Provide just the information which function to call.
- Closures: Additionally allow referencing (“capturing”) variables from outside the scope of where the closure is defined.
Basic syntax example:
#![allow(unused)] fn main() { let greeting = "Hello"; //will be captured let my_closure = |name| { format!("{greeting}, {name}!") }; let hello_world = my_closure("World"); }
Capturing can be thought of as a second point in time, when parameters can be passed into a closure, meaning ownership and borrowing rules still apply.
How the variables are captured (&, &mut, with ownership) changes how the closure or function pointer can be called, which is reflected in the type:
fn (function pointer)
#![allow(unused)] fn main() { fn my_function(name: String) -> String { //nothing is captured, only passed in as parameter when invoked format!("Hello, {name}") } takes_function_pointer(my_function); fn takes_function_pointer(function_pointer: fn(String) -> String) { function_pointer(String::from("World")); } // Can also be defined anonymously: takes_function_pointer(|name| { format!("Hello, {name}") }); }
API-Example: n/a
Fn closure
#![allow(unused)] fn main() { let greeting = "Hello"; //captured as &T let my_closure = |name| { format!("{greeting}, {name}") }; takes_Fn_closure(my_closure); fn takes_Fn_closure(closure: impl Fn(String) -> String) { closure(String::from("World")); } }
Cannot move closure outside the scope of captured variables:
#![allow(unused)] fn main() { /// Does not compile! let my_closure = { let greeting = "Hello"; //captured as &T || { println!("{greeting}") } }; // `greeting` goes out of scope here, when `closure` still has a reference on it }
API-Example: rayon::iter::ParallelIterator::map
FnMut closure
#![allow(unused)] fn main() { let mut greeting = String::from("Hello"); //captured as &mut T takes_FnMut_closure(|| { greeting.push_str(", World"); }); fn takes_FnMut_closure(mut closure: impl FnMut()) { closure(); closure(); //can call sequentially, but not in parallel (only one &mut can exist at a time) } }
API-Example: Iterator::map
FnOnce closure
#![allow(unused)] fn main() { let greeting = String::from("Hello"); //captured as (owned) T let closure = || { std::mem::drop(greeting); }; takes_FnOnce_closure(closure); fn takes_FnOnce_closure(closure: impl FnOnce()) { closure(); // closure(); //can only call once } }
API-Example: thread::spawn
Overview
| Callee captures | Resulting Type | Caller can call |
|---|---|---|
| nothing | fn (function pointer) | Anywhere and however they want |
via & | Fn closure | Only in the same scope as the captured variables |
via &, &mut | FnMut closure | Only sequentially (and in the same scope) |
via &, &mut, ownership | FnOnce closure | Only once (and in the same scope) |
ℹ️ There is an inverse relationship between callee freedom and caller freedom.
Tips
-
When designing an API, start out by accepting an
impl FnOnce. If you need to call the closure more than once, then go forimpl FnMutand so on.
The hierarchy is:fn : Fn : FnMut : FnOnce
This meansFnOnceis a super-trait to all closures and function pointers. -
The type of a closure can be annotated like a normal function:
#![allow(unused)] fn main() { let closure = |input: u8| -> String { todo!() }; }This is typically only necessary when type inference cannot decide a type.
-
movecan be used force capturing via ownership. This severs lifetimes, making the closure easier to work with when storing in a struct, when executing it in a new thread or when moving to a different scope:#![allow(unused)] fn main() { /// Does compile, thanks to `move`! let closure = { let greeting = "Hello"; //captured as T move || { println!("{greeting}") } }; // `greeting` was moved into the closure, so does not go out of scope } -
If you need to pass a value into multiple closures, it can be useful to wrap the value into an
Arc<_>(with aMutex<_>orRwLock<_>inside, if you need mutability). This makes the reference behave like in a garbage-collected language, where it can remain in the closure indefinitely, without handling lifetimes.
Async Closures
async closures can be used to run async code in a closure:
#![allow(unused)] fn main() { let mut list = vec![]; //will be captured let closure = async || { list.push( std::future::ready(String::from("Hello")).await ); }; }
These differ from returning an async block from a normal closure (e.g. || async {}) in that execution of the closure and the async code happens at the same time. This allows accessing captured values within the async code without cloning.
Previously, you would have had to use an Arc<Mutex<_>> to make this specific code example work, since it can both be cloned and allows mutating the value inside:
#![allow(unused)] fn main() { use std::sync::{Arc, Mutex}; let list = Arc::new(Mutex::new(vec![])); //will be captured let closure = || async { //without async closure list.lock().unwrap() .push( std::future::ready(String::from("Hello")).await ); }; }