Skip to navigation
A guide to closures in Rust
14.04.26
```md # A guide to closures in Rust **Date:** 2023-05-22 ## Introduction A closure is like an anonymous function. It can be called, just like a function. However, it has a couple of differences that make it suitable for use in situations where a function would be a poor fit. First, you can write it inline, which makes the code very concise. Second, it can capture variables from outside its scope. This high-level description might make you believe that closures are simple, but there's more to them than you might think. Don't worry, though; in this post, we will explore in detail what closures are and when to use them. ## What is a closure If you had a function like this: ```rust fn add(a: i32, b: i32) -> i32 { a + b } ``` And you wanted to write a closure similar to it, you would write: ```rust |a: i32, b: i32| -> i32 { a + b }; ``` Its syntax is mostly similar to the function, apart from a few things. First, there is no `fn` keyword. Second, it doesn't have a name. And third, we write the arguments inside the `|` characters instead of `(` and `)`. Since it doesn't have a name, you need to assign it to a variable before you can call it: ```rust // Assign closure to the add_closure variable let add_closure = |a: i32, b: i32| -> i32 { a + b }; // Call it let sixty_six = add_closure(42, 24); ``` That was the most verbose way of writing a closure. Quite a few elements of a closure are optional. For example, you can omit the type annotations: ```rust // Types for a and b and return value omitted let add_closure = |a, b| { a + b // return type inferred from this expression }; // Types for a and b inferred as i32 from the call site let sixty_six = add_closure(42, 24); ``` The types were inferred from where the closure was called and from its return expression. You can also omit the `{` and `}` braces if the body of the closure is a single expression: ```rust // Braces omitted let add_closure = |a, b| a + b; let sixty_six = add_closure(42, 24); ``` Removing the optional parts made the closure very compact. That's it about how you write closures. Pretty boring, right? Looks like closures are just anonymous functions with some syntactical differences. Fortunately, that's not the entire picture. Closures have a superpower that functions don't: they can capture variables from their environment. What do I mean by that? Let's take a look: ```rust let i = 42; let capture_i = || println!("{i}"); capture_i();// prints 42 ``` Here, the `capture_i` closure prints the value of the variable `i` (duh!). But this illustrates a key capability of closures. If you look closely, `i` is not a variable passed to or defined inside the closure. `i` exists outside the closure, and yet the closure can access it. This ability to access variables from a scope outside of the closure's definition is called capturing from the environment. To prove that this capacity is exclusive to closures, try capturing a variable in a function: ```rust fn foo() { let i = 42; fn bar() { // ERROR: can't capture dynamic environment in a fn item // help: use the `|| { ... }` closure form instead println!("{i}"); } } ``` The compiler helpfully tells us that if your intention was to capture `i` from the environment, use a closure. ## When to use a closure This is all great, but when would you use a closure? I mean, why would you write this: ```rust let i = 42; let capture_i = || println!("{i}"); capture_i(); ``` When you can write this: ```rust let i = 42; println!("{i}"); ``` Much simpler, right? Well, the examples above were a little contrived. A real closure usage would look more like this: ```rust fn create_adder(a: i32) -> impl Fn(i32) -> i32 { move |b| a + b } let add_5 = create_adder(5); println!("{}", add_5(4)); // prints 9 println!("{}", add_5(20)); // prints 25 ``` A lot is going on here, so let's break it down. First, we have a function `create_adder` which returns a closure. The closure returned by `create_adder` captures `a`. Then we call `create_adder` by passing `5` for `a`. This gives us a closure which adds `5` to whatever we pass to it. Then we call this closure twice and print the results. A few pieces of new syntax are worth discussing here. First, notice the `move` keyword in front of the closure. I'll explain what the `move` keyword does later in the article. For now, just trust me that it is needed for the code to compile. Next, notice the return value of `create_adder`: `Fn(i32) -> i32`. `Fn` is a trait implemented by the closure we return. Again, I will talk about closure traits in much more detail later in the article. For now, the important bit to appreciate is how closure trait syntax is different from other traits. Normally, if we have a trait like `Copy`, we just use its name: `impl Copy`. But a closure trait includes the signature of the closure as well. So a closure like `|a: i32, b: u32| -> usize` implements a trait which is written like: `Fn(i32, u32) -> usize`. And the trait for a closure with no arguments or return value is written like `Fn()`. Above, you could see that the `add_5` closure carried around a logic of "adds five" inside it. It is this power of passing around logic that makes closures so unique. They are very useful in allowing a caller to decide what logic executes inside the guts of a function. For example, if you want something like this: ```rust fn some_fn() { // line1 always executes the same logic // line2 caller decides what logic this line executes // line3 always executes the same logic } ``` Then receiving a closure in an argument and calling that in line2 will get you what you want. To show you a concrete example, the `unwrap_or_else` method on `Option` looks like this (simplified for clarity): ```rust impl
Option
{ fn unwrap_or_else
(self, f: F) -> T where F: FnOnce() -> T, { match self { Some(x) => x, None => f(), } } } ``` Here, you can see that the closure `f` is only called when `self` is `None`. The caller decides what `f` will return; the rest of the logic is always the same. Another example from the standard library where closures are used extensively is the `Iterator` trait. The code inside the body of a closure influences two aspects of the closure – how a closure captures variables from their environment and which closure traits does the closure implement. Let's see how. ## How closures capture their environment For a minute, let's go back to the following example you saw before: ```rust let i = 42; let capture_i = || println!("{i}"); capture_i();// prints 42 ``` Here, the closure's body prints `i`. So it needs to only immutably borrow `i`. How do we know? To confirm, we can try to use `i` between the closure definition and the call to closure by, for example, adding a `println!` between these two lines: ```rust let i = 42; let capture_i = || println!("Inside closure: {i}"); println!("Outside closure: {i}");// <-- New line addded capture_i(); ``` If `i` is borrowed immutably, the code should compile; otherwise, the borrow checker should shout at us. Since the above code compiles, we can say that `i` is captured immutably, right? Not so fast. Although the code compiles, we have a flaw in our reasoning. Can you spot what it is? Because `i` is `i32`, which is `Copy`, the closure could have just copied `i`. We haven't proved that `i` is immutably borrowed. We need a type which is not copy. A `String` would do: ```rust let i: String = "42".to_string();// i is now a String, a non-copy type let capture_i = || println!("Inside closure: {i}"); println!("Outside closure: {i}"); capture_i(); ``` This code works, so finally, we have proved that `i` is only immutably borrowed by the above closure. That is the first way closures capture variables: by borrowing immutably. But those are not the only kind of closures. Consider this: ```rust let mut animal = "fox".to_string(); let mut capture_animal = || { animal.push_str("es"); }; // ERROR: cannot borrow `animal` as immutable because it is also borrowed as mutable println!("Outside closure: {animal}"); capture_animal(); ``` This code is very similar to the previous one, but here the closure mutates the string `animal` by pushing a suffix "es" to it. Which means the closure should mutably borrow `animal`. And indeed, it does because the borrow checker complains about the `println!` statement. To make the code compile, we need to move this line after the call to the closure: ```rust let mut animal = "fox".to_string(); let mut capture_animal = || { animal.push_str("es"); }; capture_animal(); // mutable borrow ends here println!("Outside closure: {animal}"); // Ok to use animal here ``` That is the second way closures capture: by borrowing mutably. The third way closures capture variables is by moving them inside their bodies. For example: ```rust let animal = "fox".to_string(); let capture_animal = || { println!("Dropping {animal}"); drop(animal); }; capture_animal(); ``` Here, the `animal` is dropped inside the closure. This means the following wouldn't work: ```rust let animal = "fox".to_string(); let capture_animal = || { println!("Dropping {animal}"); drop(animal); }; // ERROR: borrow of moved value: `animal` println!("Outside closure: {animal}"); capture_animal(); ``` And neither would moving the `println!` statement after the closure call: ```rust let animal = "fox".to_string(); let capture_animal = || { println!("Dropping {animal}"); drop(animal); }; capture_animal(); // ERROR: borrow of moved value: `animal` println!("Outside closure: {animal}"); ``` This makes sense because `animal` is moved into the closure. Borrow checker has every right to call out this code as unsound. In summary, closures capture variables from their environment by either immutably borrowing, mutably borrowing, or moving. Not too different from how the rest of Rust works. Finally, let's talk about that `move` keyword you saw earlier. Remember this example from before in which the closure captured `i` by immutable borrow: ```rust let i: String = "42".to_string(); let capture_i = || println!("Inside closure: {i}"); println!("Outside closure: {i}"); capture_i(); ``` All the `move` keyword does is force the closure to take ownership. ```
https://hashrust.com/blog/a-guide-to-closures-in-rust/
Reply
Anonymous
Information Epoch 1785961541
Store numerical data in flat files.
Home
Notebook
Contact us