A guide to closures in Rust
2023-05-22
Introduction
A closure is like an anonymous function. It can be called, like a function can be called. But it has a couple of dissimilarities with a function that make it suitable for use in situations where a function will be a poor fit. First, you can write it inline which makes the code very concise. And 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 about what closures are and when to use them.
What is a closure
If you had a function like this:
a + b
And you wanted to write a closure similar to it, you will write:
|a: i32, b: 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:
//Assign closure to the add_closure variable
let add_closure =
a + b
;
//Call it
let sixty_six = add_closure;
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:
//Types for a and b and return value omitted
let add_closure =
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;
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:
//Braces omitted
let add_closure = ;
let sixty_six = add_closure;
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:
let i = 42;
let capture_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:
let i = 42;
//ERROR:can't capture dynamic environment in a fn item
//help: use the `|| { ... }` closure form instead
println!;
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:
let i = 42;
let capture_i = ;
capture_i;
When you can write this:
let i = 42;
println!;
Much simpler, right. Well, the examples above were a little contrived. A real closure usage would look more like this:
move |b| a + b
let add_5 = create_adder;
println!; //prints 9
println!; //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 is 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:
//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):
F: FnOnce() -> T,
match self
Some => 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:
let i = 42;
let capture_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:
let i = 42;
let capture_i = ;
println!;// <-- 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:
let i: String = "42".to_string;// i is now a String, a non-copy type
let capture_i = ;
println!;
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:
let mut animal = "fox".to_string;
let mut capture_animal =
animal.push_str;
;
//ERROR:cannot borrow `animal` as immutable because it is also borrowed as mutable
println!;
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:
let mut animal = "fox".to_string;
let mut capture_animal =
animal.push_str;
;
capture_animal; // mutable borrow ends here
println!; // 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:
let animal = "fox".to_string;
let capture_animal =
println!;
drop;
;
capture_animal;
Here the animal
is dropped inside the closure. This means the following wouldn't work:
let animal = "fox".to_string;
let capture_animal =
println!;
drop;
;
//ERROR:borrow of moved value: `animal`
println!;
capture_animal;
And neither would moving the println!
statement after the closure call:
let animal = "fox".to_string;
let capture_animal =
println!;
drop;
;
capture_animal;
//ERROR:borrow of moved value: `animal`
println!;
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:
let i: String = "42".to_string;
let capture_i = ;
println!;
capture_i;
All the move
keyword does is force the closure to take ownership of the variable:
let i: String = "42".to_string;
// added move before closure
let capture_i = move || println!;
println!;// <-- comment out this line to compile
capture_i;
Once the move
keyword forces i
to be moved into the closure, the usual caveats apply. So you should know why the second println!
statement in the code above needs to be commented out. Having understood that, can you puzzle out why the following works:
let i: String = "42".to_string;
let i_ref = &i;
let capture_i = move || println!;
println!;
println!;
capture_i;
Here we are moving a reference into the closure. So i
is essentially immutably borrowed by this closure even though we used the move
keyword. This is a neat trick to remember for when you have more than one variable being captured by a closure and you want some of them to move into the closure but others only immutably/mutably borrowed. But when would you want to force variables to move into the closure by using the move
keyword? Take this example:
//ERROR:closure may outlive the current function, but it borrows `animal`, which is owned by the current function
||
animal.push_str;
println!;
let mut pluralize_fox = create_pluralizer;
pluralize_fox;
Here the closure captures animal
by mutable borrow. But that is not sufficient. Because animal
is moved into the create_pluralizer
function, it is dropped at its end. So later when the returned closure is called it will access a dropped animal
. A big no no. To fix this the compiler suggests adding the move
keyword in front of the closure:
move ||
animal.push_str;
println!;
let mut pluralize_fox = create_pluralizer;
pluralize_fox;
Which does fix the issue. Another time you would want to use move
is when you are spawning a new thread. A new thread takes a closure and if you want to force move some data to the new thread, you can use the move
keyword:
let animal = "fox".to_string;
spawn
.join
.unwrap;
But don't worry too much about this keyword. Most of the time when the compiler suggests to use it, you can mechanically follow its advice and you won't go wrong.
How closures implement the closure traits
There are three traits, Fn
, FnMut
and FnOnce
, that the compiler automatically implements for closures based on what the code inside them does:
- Compiler implements
Fn
for those closures which do not mutate or move the captured variables out of the closure. This also includes those closures which do not capture any variables at all. Such a closure can be called multiple times, even in parallel on multiple threads (though you might need additional bounds likeSend
,Sync
and'static
). - Compiler implements
FnMut
for those closures which mutate any captured variables but don't move them out of the closure. Such a closure can be called multiple times but not in parallel on multiple threads. - Compiler implements
FnOnce
for those closures which move the captured variables out of the closure, for example by dropping them. Such a closure can be called only once because after calling it the first time it no longer owns the captured variable. Note that themove
keyword does not affect whether this trait is implemented or not.move
only forces the captured variables to be moved into the closure whileFnOnce
is implemented if the captured variable is moved out.
The Fn
trait is a subtrait of FnMut
and the FnMut
trait is a subtrait of FnOnce
. This has a few implications:
- Every closure implementing
Fn
trait implementsFnMut
as well asFnOnce
. Which means anywhere anFnMut
orFnOnce
is expected a closure implementingFn
can be passed. - Every closure implementing
FnMut
trait implementsFnOnce
. Which means anywhere anFnOnce
is expected a closure implementingFnMut
can be passed.
These traits are critical in using closures because the concrete type of a closure can't be written:
let closure/*:No way to write the type of closure here*/ = ;
Each closure has an anonymous type assigned to it by the compiler. Even two identical closures have different types:
let mut closure1 = ;
let closure2 = ;
closure1 = closure2;//ERROR:expected closure, found a different closure
So the only way to refer to closures is through these traits.
Now let's say we have a function that takes an Fn
closure as an argument and calls it:
c;
c;
As you can see, a closure which implements the Fn
trait can be called multiple times. This trait is implemented automatically by those closures which either capture nothing or capture by an immutable borrow. For example:
let i = 42;
// Both capture_nothing and capture_i implement 'Fn'
let capture_nothing = ;
let capture_i = ;
call_closure;
call_closure;
But if we try to mutate the captured variables, things no longer work:
let mut i = 42;
call_closure
i += 1;//ERROR:cannot assign to `i`, as it is a captured variable in a `Fn` closure
println!
;
This makes sense, the call_closure
method expects an Fn
closure which guarantees that it won't mutate the state. But our closure is mutating the state. The compiler suggests that we change the trait bound in call_closure
to FnMut
instead. Let's try that:
//ERROR:cannot borrow `c` as mutable, as it is not declared as mutable
c;
c;
It still doesn't work. Why does the compiler want c
to be mutable? This has to do with how closures are implemented by the compiler which is discussed later in the article. For now let's just blindly follow the compiler and make c
mutable:
c;
c;
This finally works. The callee expects an FnMut
and the caller passes an FnMut
. So what happens if we pass an Fn
closure to a function expecting an FnMut
closure:
c;
c;
let i = 42;
//This is an Fn closure because it doesn't mutate the captured i
let fn_closure = ;
call_closure;
This still works because the FnMut
trait bound says that the caller may pass a closure that mutates, not that they have to. In other words, this works because Fn
is a subtrait of FnMut
. Which means that all closures which implement Fn
also implement FnMut
.
Let's extend the example one more time:
//ERROR:cannot move out of `i`, a captured variable in an `FnMut` closure
let i = "42".to_string;// i is a String now
call_closure
println!;
drop;// i is dropped in the closure
;
This time we drop i
inside the closure. Though the compiler error is not as clear this time, we can figure out what is wrong. Notice that call_closure
calls the closure twice, but we are passing a closure that drops i
. That would be a double free if allowed. We are passing a FnOnce
closure to a function that expects an FnMut
closure. So let's fix it by calling the closure only once and changing the bound to FnOnce
:
c;//Remove the second call to c()
The FnOnce
trait bound means that the caller might pass a closure that can be called only once. They are not forced to. For example passing an FnMut
works:
c;
let mut i = "fox".to_string;
//This is an FnMut closure because it just mutates the captured i
let fn_mut_closure =
i.push_str;
;
call_closure;
And so does passing an Fn
:
c;
let i = 42;
//This is an Fn closure because it doesn't mutate the captured i
let fn_closure = ;
call_closure;
These work is because both Fn
and FnMut
are subtraits of FnOnce
. An important point to note is that the move
keyword doesn't necessarily mean that the closure will implement the FnOnce
trait. For example, the following closure implements Fn
even though move
is used:
let i = "42".to_string;
let capture_i = move ||
println!;
;
Which trait is implemented is decided by what the closure does with the captured variable, not how it is captured. move
only forces the captured variable to be moved into the closure while FnOnce
is implemented if the closure moves the captured variable out. Apart from dropping a captured variable, another example of moving a captured variable out of the closure is the following:
let i = "42".to_string;
let mut strings = Vec new;
let capture_i: &dyn FnOnce = &move ||
strings.push;//moves i out of the closure to the strings vec
;
Here the captured i
is pushed into strings
vec. This moves i
out of the closure which makes it implement FnOnce
. Even if you remove the move
keyword, it is still FnOnce
:
let i = "42".to_string;
let mut strings = Vec new;
//capture_i is still FnOnce
let capture_i: &dyn FnOnce = &||
strings.push;
;
move
keyword made no difference to which trait was implemented.
In the examples above you might have noticed a tug-of-war between the caller and callee. If the callee accepts FnOnce
it gets the least freedom in what it can do with the closure – it can just call it once. But the caller is free to pass any closure at all. Because all closures implement FnOnce
by virtue of the subtrait/supertrait relationships.
If the callee takes an FnMut
it gets a little more leeway. Now it can call the closure multiple times, but it still can't, for example, call the closure concurrently on multiple threads, because that would lead to data races. The caller in turn gets restricted a little bit more. It can't, for example, drop a captured variable or otherwise move it outside the closure.
And finally when the callee takes an Fn
it is free to do almost anything with it – call it multiple times, even on different threads concurrently. The caller instead has its hands tied in that it can't even mutate the captured variables in the closure.
This inverse relationship between how much freedom does the caller and callee have should inform your API design. Try to give as much power to the caller as possible by accepting the least restrictive trait bounds; that is, prefer taking an FnOnce
to FnMut
and an FnMut
to an Fn
.
Function pointers
A function pointer is just that: a pointer to a function. For example:
a + b
let func_ptr: fn = add;//notice small 'fn' instead of capital 'Fn'
Here the func_ptr
is a function pointer which points to the add
function. Note the type of the function pointer: fn(i32, i32) -> i32
. It starts with a lowercase f. Unlike a closure trait a function pointer type is not a trait.
Closures which don't capture any variable at all are like functions. Hence such closures can be coerced into function pointers:
let func_ptr: fn = ; //right side is a closure now
But if the closure captures a variable, it can no longer be coerced:
let i = 42;
//ERROR: closures can only be coerced to `fn` types if they do not capture any variables
let func_ptr: fn = ;
And since functions can't capture any environment, they are like an Fn
closure that captures nothing. And because Fn
is the lowest in the trait hierarchy, all function pointers implement all the closure traits:
c;
c;
c;
a + b
call_fn_once;//compiles ok
call_fn_mut;//so does this
call_fn;//and this
So when should you use a function pointer and when a closure? Your first choice should be a closure because it gives the caller more freedom in what to pass. For example, the call_fn
can accept both a closure and a function pointer:
c;
a + b
call_fn;//ok
call_fn;//also ok
There are times when using a function pointer makes sense. For example, when you are calling C code through FFI it's ok to use function pointers because C doesn't understand Rust closures. Another reason to pick a function pointer can be performance. Since closures can only be used through closure traits, all the usual tradeoffs for traits apply. You have to pick between static vs dynamic dispatch, for example. You can sidestep this concern by using function pointers. But measure before you go this route. In short, prefer closures and use function pointers only when you have to.
How the compiler implements closures
Even if you understood everything until now, you might have nagging questions about how closures actually work. What does it mean to pass a closure around? Where does it store captured variables? Well it's quite simple, the compiler uses a struct to store the captured variables. For example a closure like this:
let i = 42;
let capture_i = ;
capture_i;
Is desugared into something like this:
//Doesn't compile, only for illustration
i: &'a i32
println!;
let i = 42;
let capture_i = Closure ;
capture_i.call;
Here the closure captures a reference to i
that is why the Closure
struct's i
has a type &'a i32
. Then the compiler implements the Fn
trait for this struct and calling the closure calls the call
method of the Fn
trait. The idea is similar for closures which capture variables by mutable reference and which capture by taking ownership.
You are now in a position to understand why we had to make c
mutable for an FnMut
closure. The example where we did this is replicated below for convenience:
c;
c;
We had to make c
mutable because the FnMut
trait is defined something like this:
;
(The actual FnMut
is defined like this, I simplified it for ease of understanding). The call_mut
method takes self
by &mut
which is why compiler wants c
to be mut
. If you want to dig deeper into closures, you can take a look at an explanation of how closures are expanded by the compiler. Or take a look at their actual representation in rustc.
Conclusion
Closures in Rust, like many other languages, are a useful tool to make your APIs more fluent and the code more concise and expressive. But due to Rust's unique safety guarantees they are also a little harder to master. Hopefully this article gave you a thorough understanding in them. Please 🙏 don't forget to share on Twitter 👇.