About closure
Closure in Rust, also called lamba expressions or lambdas, are functions that can capture the enclosing environment.
Some characterisitics of closures include:
- using
||instead of()around input variables. - optional body delimination
{}for a single expression (mandatory otherwise). - the ability to capture the outer environment variables
example
1 | fn main() { |
Capturing
Capturing can flexibly adapt to the use case, sometimes moving and sometimes borrowing. Closures can capture variables:
- by reference:
&T - by mutable reference:
&mut T - by value:
T
Closures preferentially capture variables by reference and only go lower when required.
example
1 | fn main() { |
Howerver, if we do this:
1 | fn main() { |
We would get:
1 | error[E0382]: use of moved value: `consume` |
mem::drop requires T so this mut take by value. A copy type would copy into the closure leaving the original untouched. A non-copy mut move and so movable immediately moves into the closure.
move
Using move before vertical pipes forces closures to take ownership of captured variables:
example
1 | fn main() { |
output
1 | true |
If we call vec.len() later:
1 | fn main() { |
We would get:
1 | error[E0382]: borrow of moved value: `vec` |
It’s no doubt that vec has been moved into the closure.
As input parameters
While Rust choose how to capture variables on the fly mostly without type annotaions, this ambiguity is not allowed when writing functions. When taking a closure as an input parameter, the closure’s complete type must be annotated using one of a few traits. In order of decreasing restriction, they are:
Fn: the closure captures by reference(&T)FnMut: the closure captures by mutable reference(&mut T)FnOnce: the closure captures by value(T)
For instance, consider a parameter annotated as FnOnce. This specifies that the closure may caputure by &T, &mutT, or T, but ultimately choose based on how the captured variables are used in the closure.
This is because if a move is possible, then any type of borrow should also be possible. Note that reverse is not true. If the paramenter is annotated as Fn, then capturing varibales by &mut T or T are not allowed.
example
1 | fn apply<F>(f: F) where |
Type anonymity
Using a closure as a parameter requires generics.This is necessary because of how thet are defined:
1 | // `F` must be generic. |
When a closure is defined, the compiler implicitly creates a new anonymous structure to store the captured variables inside, meanwhile implementing the functionality via one of the traits: Fn, FnMut or FnOnce for this unknown type. This type is assigned to the variable which is stored until calling.
Since this new type if of unknown type, any usage in a function will require generics. However, an unbounded type parameter traits: Fn, FnMut or FnOnce(which it implements) is sufficient to specify its type.
Input functions
If you declare a function that takes a closure as a parameter, then any function that satisfies the trait bound of that closure can be passed as parameter.
example
1 | fn call_me<F>(f: F) |
function could also be used as a parameter as the same as closure.
As output parameters
Returning closures as output parameters are also possible. However, anoymous closure types are, by definition, unknown, so we have to use impl Trait to return them.
example
1 | fn create_fn() -> impl Fn() { |
Note: impl xxTrait means a type that implements xxTrait.