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:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

And you wanted to write a closure similar to it, you will write:

|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:

//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:

//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:

//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:

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:

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:

let i = 42;
let capture_i = || println!("{i}");
capture_i();

When you can write this:

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:

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 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:

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):

impl<T> Option<T> {
    fn unwrap_or_else<F>(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:

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:

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:

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:

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:

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:

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:

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:

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:

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 of the variable:

let i: String = "42".to_string();
// added move before closure
let capture_i = move || println!("Inside closure: {i}");
println!("Outside closure: {i}");// <-- 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!("{i_ref}");
println!("{i}");
println!("{i_ref}");
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:

fn create_pluralizer(mut animal: String) -> impl FnMut() {
//ERROR:closure may outlive the current function, but it borrows `animal`, which is owned by the current function
    || {
        animal.push_str("es");
        println!("Pluralized animal: {animal}");
    }
}

let mut pluralize_fox = create_pluralizer("fox".to_string());
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:

fn create_pluralizer(mut animal: String) -> impl FnMut() {
    move || { // <- move keyword added
        animal.push_str("es");
        println!("Pluralized animal: {animal}");
    }
}

let mut pluralize_fox = create_pluralizer("fox".to_string());
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();

std::thread::spawn(move || println!("Animal owned by this thread: {animal}"))
    .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:

  1. 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 like Send, Sync and 'static).
  2. 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.
  3. 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 the move keyword does not affect whether this trait is implemented or not. move only forces the captured variables to be moved into the closure while FnOnce 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:

  1. Every closure implementing Fn trait implements FnMut as well as FnOnce. Which means anywhere an FnMut or FnOnce is expected a closure implementing Fn can be passed.
  2. Every closure implementing FnMut trait implements FnOnce. Which means anywhere an FnOnce is expected a closure implementing FnMut 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:

fn call_closure<C: Fn()>(c: C) {
    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 = || println!("I capture nothing");
let capture_i = || println!("I capture i immutably: {i}");
call_closure(capture_nothing);
call_closure(capture_i);

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!("{i}")
});

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
fn call_closure<C: FnMut()>(c: C) {//Update bound on C to FnMut
    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:

fn call_closure<C: FnMut()>(mut c: C) {//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:

fn call_closure<C: FnMut()>(mut c: C) {
    c();
    c();
}
let i = 42;
//This is an Fn closure because it doesn't mutate the captured i
let fn_closure = || println!("{i}");
call_closure(fn_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!("Dropping {i}");
    drop(i);// 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:

fn call_closure<C: FnOnce()>(c: C) {//Update bound on C 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:

fn call_closure<C: FnOnce()>(c: C) {
    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("es");
};
call_closure(fn_mut_closure);

And so does passing an Fn:

fn call_closure<C: FnOnce()>(c: C) {
    c();
}
let i = 42;
//This is an Fn closure because it doesn't mutate the captured i
let fn_closure = || println!("{i}");
call_closure(fn_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!("{i}");
};

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(i);//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(i);
};

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:

fn add(a: i32, b: i32) -> i32 {
    a + b
}
let func_ptr: fn(i32, i32) -> i32 = 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(i32, i32) -> i32 = |a, b| a + b; //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(i32, i32) -> i32 = |a, b| a + i;

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:

fn call_fn_once<C: FnOnce(i32, i32) -> i32>(c: C) {
    c(1, 2);
}

fn call_fn_mut<C: FnMut(i32, i32) -> i32>(mut c: C) {
    c(1, 2);
}

fn call_fn<C: Fn(i32, i32) -> i32>(c: C) {
    c(1, 2);
}

fn add(a: i32, b: i32) -> i32 {
    a + b
}

call_fn_once(add);//compiles ok
call_fn_mut(add);//so does this
call_fn(add);//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:

fn call_fn<C: Fn(i32, i32) -> i32>(c: C) {
    c(1, 2);
}

fn add(a: i32, b: i32) -> i32 {
    a + b
}

call_fn(add);//ok
call_fn(|a, b| a + b);//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 = || println!("{i}");
capture_i();

Is desugared into something like this:

//Doesn't compile, only for illustration
struct Closure<'a> {
    i: &'a i32
}

impl Fn for Closure<'_> {
    fn call(&self) {
        println!("{}", self.i);
    }
}

let i = 42;
let capture_i = Closure { i: &i };
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:

fn call_closure<C: FnMut()>(mut c: C) {//make c mutable
    c();
    c();
}

We had to make c mutable because the FnMut trait is defined something like this:

pub trait FnMut : FnOnce
{
    fn call_mut(&mut self);
}

(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 👇.