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:

Filename: src/main.rs

fn main() {
    let add_one = |x| x + 1;

    let five = add_one(4);

    assert_eq!(5, five);
}

Listing 13-1: A closure that takes one parameter and adds one to it, assigned to the variable add_one

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:

Filename: src/main.rs

fn main() {
    let calculate = |a, b| {
        let mut result = a * 2;

        result += b;

        result
    };

    assert_eq!(7, calculate(2, 3)); // 2 * 2 + 3 == 7
    assert_eq!(13, calculate(4, 5)); // 4 * 2 + 5 == 13
}

Listing 13-2: A closure with two parameters and multiple expressions in its body

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:

Filename: src/main.rs

fn main() {
    let add_one = |x: i32| -> i32 { x + 1 };

    assert_eq!(2, add_one(1));
}

Listing 13-3: A closure definition with optional parameter and return value type annotations

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:

Filename: src/main.rs

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

Listing 13-4: Example of a closure that refers to a variable in its enclosing scope

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.

results matching ""

    No results matching ""