Closures
Rust gives you the ability to define closures, which are similar to
functions. Instead of starting with a technical definition, let's see what
closures look like, syntactically, and then we'll return to defining what they
are. Listing 13-1 shows a small closure whose definition is assigned to the
variable add_one
, which we can then use to call the closure:
The closure definition, on the first line, shows that the closure takes one
parameter named x
. Parameters to closures go in between vertical pipes (|
).
This is a minimal closure with only one expression as its body. Listing 13-2 has a closure with a bit more complexity:
We can use curly brackets to define a closure body with more than one expression.
You'll notice a few things about closures that are different from functions
defined with the fn
keyword. The first difference is that we did not need to
annotate the types of the parameters the closure takes or the value it returns.
We can choose to add type annotations if we want; Listing 13-3 shows the
closure from Listing 13-1 with annotations for the parameter's and return
value's types:
The syntax of closures and functions looks more similar with type annotations. Let's compare the different ways we can specify closures with the syntax for defining a function more directly. We've added some spaces here to line up the relevant parts:
fn add_one_v1 (x: i32) -> i32 { x + 1 } // a function
let add_one_v2 = |x: i32| -> i32 { x + 1 }; // the full syntax for a closure
let add_one_v3 = |x| { x + 1 }; // a closure eliding types
let add_one_v4 = |x| x + 1 ; // without braces
The reason type annotations are not required for defining a closure but are required for defining a function is that functions are part of an explicit interface exposed to your users, so defining this interface rigidly is important for ensuring that everyone agrees on what types of values a function uses and returns. Closures aren't used in an exposed interface like this, though: they're stored in bindings and called directly. Being forced to annotate the types would be a significant ergonomic loss for little advantage.
Closure definitions do have one type inferred for each of their parameters and
for their return value. For instance, if we call the closure without type
annotations from Listing 13-1 using an i8
, we'll get an error if we then try
to call the same closure with an i32
:
Filename: src/main.rs
let add_one = |x| x + 1;
let five = add_one(4i8);
assert_eq!(5i8, five);
let three = add_one(2i32);
The compiler gives us this error:
error[E0308]: mismatched types
-->
|
7 | let three = add_one(2i32);
| ^^^^ expected i8, found i32
Since closures' types can be inferred reliably since they're called directly, it would be tedious if we were required to annotate their types.
Another reason to have a different syntax from functions for closures is that they have different behavior than functions: closures possess an environment.
Closures Can Reference Their Environment
We've learned that functions can only use variables that are in scope, either
by being const
or being declared as parameters. Closures can do more: they're
allowed to access variables from their enclosing scope. Listing 13-4 has an
example of a closure in the variable equal_to_x
that uses the variable x
from the closure's surrounding environment:
Here, even though x
is not one of the parameters of equal_to_x
, the
equal_to_x
closure is allowed to use x
, since x
is a variable defined in
the scope that equal_to_x
is defined. We aren't allowed to do the same thing
that Listing 13-4 does with functions; let's see what happens if we try:
Filename: src/main.rs
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool { z == x }
let y = 4;
assert!(equal_to_x(y));
}
We get an error:
error[E0434]: can't capture dynamic environment in a fn item; use the || { ... }
closure form instead
-->
|
4 | fn equal_to_x(z: i32) -> bool { z == x }
| ^
The compiler even reminds us that this only works with closures!
Creating closures that capture values from their environment is mostly used in the context of starting new threads. We'll show some more examples and explain more detail about this feature of closures in Chapter 16 when we talk about concurrency.
Closures as Function Parameters Using the Fn
Traits
While we can bind closures to variables, that's not the most useful thing we
can do with them. We can also define functions that have closures as parameters
by using the Fn
traits. Here's an example of a function named call_with_one
whose signature has a closure as a parameter:
fn call_with_one<F>(some_closure: F) -> i32
where F: Fn(i32) -> i32 {
some_closure(1)
}
let answer = call_with_one(|x| x + 2);
assert_eq!(3, answer);
We pass the closure |x| x + 2
, to call_with_one
, and call_with_one
calls
that closure with 1
as an argument. The return value of the call to
some_closure
is then returned from call_with_one
.
The signature of call_with_one
is using the where
syntax discussed in the
Traits section of Chapter 10. The some_closure
parameter has the generic type
F
, which in the where
clause is defined as having the trait bounds
Fn(i32) -> i32
. The Fn
trait represents a closure, and we can add types to
the Fn
trait to represent a specific type of closure. In this case, our
closure has a parameter of type i32
and returns an i32
, so the generic bound
we specify is Fn(i32) -> i32
.
Specifying a function signature that contains a closure requires the use of generics and trait bounds. Each closure has a unique type, so we can't write the type of a closure directly, we have to use generics.
Fn
isn't the only trait bound available for specifying closures, however.
There are three: Fn
, FnMut
, and FnOnce
. This continues the patterns of
threes we've seen elsewhere in Rust: borrowing, borrowing mutably, and
ownership. Using Fn
specifies that the closure used may only borrow values in
its environment. To specify a closure that mutates the environment, use
FnMut
, and if the closure takes ownership of the environment, FnOnce
. Most
of the time, you can start with Fn
, and the compiler will tell you if you
need FnMut
or FnOnce
based on what happens when the function calls the
closure.
To illustrate a situation where it's useful for a function to have a parameter that's a closure, let's move on to our next topic: iterators.