Closures

In Chapter 4.1 we had a first look at a closure. In this chapter, we'll dive deeper into closures, especially how they can capture their environment, and the effect this has on ownership and borrowing.

What does "capturing their environment" mean? It means that a closure can use variables from the scope in which it was defined. An example will make this clearer.

fn main() {
    let name = "Marcel".to_string();
    let greeting = "Hello".to_string();

    let print_greeting = || {
        println!("{greeting}, {name}");
    };

    print_greeting();
    println!("{name}");
}

In this case the closure borrows name and greeting from the scope in which it was defined. This is called a "borrowing closure". The closure borrows the variables, and does not take ownership of them.

If you want to take ownership of a variable, you can use the move keyword. This will move the variables into the closure.

fn main() {
    let name = "Marcel".to_string();
    let greeting = "Hello".to_string();

    let print_greeting = move || {
        println!("{greeting}, {name}");
    };

    print_greeting();
}

At first glance, this looks like the previous example, but there is a subtle difference. The closure now takes ownership of name and greeting. This means that the variables are moved into the closure, and are no longer available in the outer scope.

Let's confirm this by trying to use name after the closure has been called.

fn main() {
    let name = "Marcel".to_string();
    let greeting = "Hello".to_string();

    let print_greeting = move || {
        println!("{greeting}, {name}");
    };

    print_greeting();
    println!("{name}");
}

This will result in a compilation error:

error[E0382]: borrow of moved value: `name`
  --> src/main.rs:10:15
   |
2  |     let name = "Marcel".to_string();
   |         ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
...
5  |     let print_greeting = move || {
   |                          ------- value moved into closure here
6  |         println!("{greeting}, {name}");
   |                                ---- variable moved due to use in closure
...
10 |     println!("{name}");
   |               ^^^^^^ value borrowed here after move
   |

If you have heard the sentence: "I'm fighting the borrow checker", this is what it means. The Rust compiler is preventing you from making a mistake that could lead to undefined behavior at runtime.

There are different ways to solve this issue. You could clone the variables before moving them into the closure, or you could use a reference to the variables. Let's look at both options.

fn main() {
    let name = "Marcel".to_string();
    let greeting = "Hello".to_string();

    let use_name = name.clone();
    let use_greeting = greeting.clone();

    let print_greeting = move || {
        println!("{use_greeting}, {use_name}");
    };

    print_greeting();
    println!("{name}");
}

This will work, but it's not very elegant. You have to clone the variables, and then use the clones in the closure. This is not very efficient, especially if the variables are large.

A better way is to use references. This way you don't have to clone the variables, and you can still use them in the closure.

fn main() {
    let name = "Marcel".to_string();
    let greeting = "Hello".to_string();

    let print_greeting = move |name: &str, greeting: &str| {
        println!("{greeting}, {name}");
    };

    print_greeting(&name, &greeting);
    println!("{name}");
}

This will work in many cases, but it's not always possible to use references. An almost-always foolproof way to solve this issue is to use the Rc and RefCell types. We'll look at these in the next chapter.

An Rc is a reference-counted pointer to immutable data. This means that you can have multiple references to the same data, and the data will only be dropped when the last reference is dropped. An Rc is used when you want to have multiple owners of the same data.

A RefCell is a mutable memory location with dynamically checked borrow rules. This means that you can have multiple mutable references to the same data, and the borrow checker will ensure that the references are used correctly.

Let's look at the above example using an Rc

use std::rc::Rc;

fn main() {
    let name = Rc::new("Marcel".to_string());
    let greeting = Rc::new("Hello".to_string());

    let name2 = name.clone();
    let greeting2 = greeting.clone();

    let print_greeting = move || {
        println!("{greeting2}, {name2}");
    };

    print_greeting();
    println!("{name}");
}

It looks a bit like the earlier example where we cloned the variables, but there is a subtle difference. We are not cloning the variables, we are cloning the Rc pointers. This means that we are not cloning the data, we are cloning the reference to the data. This is much more efficient, especially if the data is large.

To complete the example, we'll look at the RefCell type. We'll use the RefCell type to make the name and greeting variables mutable.

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let name = Rc::new(RefCell::new("Marcel".to_string()));
    let greeting = Rc::new(RefCell::new("Hello".to_string()));

    let name2 = name.clone();
    let greeting2 = greeting.clone();

    let print_greeting = move || {
        println!("{}, {}", greeting2.borrow(), name2.borrow());
    };

    print_greeting();
    println!("{}", name.borrow());

    // Change the name and greeting
    *name.borrow_mut() = "Alice".to_string();
    *greeting.borrow_mut() = "Goodbye".to_string();

    print_greeting();
    println!("{}", name.borrow());
}

This will print:

Hello, Marcel
Marcel
Goodbye, Alice
Alice

In this example we use the borrow and borrow_mut methods to get a reference to the data. The borrow method returns an immutable reference, and the borrow_mut method returns a mutable reference. The borrow checker will ensure that the references are used correctly.