Rust’s closures are anonymous functions you can save in a variable or pass as arguments to other functions, and unlike functions, closures can capture values from the scope in which they are defined.
let some_closure = |param1: u32, param2: u32| -> Result<(), Box<dyn std::error::Error>> {
do_something_with_params(param1, param2)?;
Ok(())
};
// Next, we cal call some_closure as a normal function.
if let Ok(_) = some_closure(123, 456) {
// do something...
}
Closures does not require one to annotate the types of the parameters or the return value like a normal function does. Type annotations are required on functions because they are part of an explicit interface exposed to users, while closures are local objects storing in variables and used without naming them and exposing them to users of the library. Closures are usually short and relevant only within a narrow context rather than in any arbitrary scenario. Within these limited contexts, the compiler is reliably able to infer the types of the parameters and the return type, similar to how it is able to infer the types of most variables. The following definitions are quasi-equivalent:
// Function
fn add_one_v1 (x: u32) -> u32 { x + 1 }
// Closures
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
Note that closures are statically types, which means if the compiler infers the type of the closure, we cannot change it anymore.
let foo = |x| x;
let s = foo(String::from("bar")); // OK.
let n = foo(5); // Error!
let n = foo(5.to_string()) // OK.
Fn
TraitsWe can create a struct that will hold the closure and the resulting value of calling the closure. The struct will execute the closure only if we need the resulting value, and it will cache the resulting value so the rest of our code doesn’t have to be responsible for saving and reusing the result. You may know this pattern as memoization or lazy evaluation.
struct Cacher<T>
where T: Fn(u32) -> u32
{
calculation: T, // Trait bound on closure type: Fn (u32) -> u32
value: Option<u32>,
}
impl<T> Cacher<T> {
// Constructor.
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}
// Calculates the value and forbids the user to directly access value.
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(val) => val,
None => {
let val = (self.calculation)(arg);
self.value = Some(val);
self.value
}
}
}
}
Note that closures will capture environment, which means they would incur memory overhead and the issue of ownership. There are three different Fn
traits as follows:
FnOnce
consumes the variables it captures from its enclosing scope. To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined. The Once
part of the name represents the fact that the closure can’t take ownership of the same variables more than once, so it can be called only once.FnMut
can change the environment because it mutably borrows values.fn
borrows the environment immutably.These traits can be inferred by the compiler when the closure is created. If you want to force the closure to take ownership of the values it uses in the environment, you can use the move
keyword before the parameter list. This technique is mostly useful when passing a closure to a new thread to move the data so it’s owned by the new thread.
let vec1 = vec![1, 2, 3];
let eq = move |x| x == vec1;
// vec is moved and cannot be used anymore.
assert!(eq(vec![1, 2, 3]));