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
.