Closures

Dear Computer

Chapter 11: Managing Memory

Closures

We can't nest functions in Java or C, but we can in Rust. This program nests function enclose inside of main:

Rust
fn main() {
  fn enclose(x: i32) -> String {
    let open = '(';
    let close = ')';
    format!("{}{}{}", open, x, close)
  }

  println!("{}", enclose(14));
}

There are two compelling reasons for a programming language to allow one function to nest inside another. The first reason is to narrow the nested function's scope. If a function has only local utility, then a local scope is more appropriate than a top-level scope. The second reason is to gain closure semantics—which means the function may access values from the surrounding context without passing them as parameters.

Do nested functions in Rust have closure semantics? We can check by migrating the delimiters of enclose out to main:

Rust
fn main() {
    let open = '(';
    let close = ')';

    fn enclose(x: i32) -> String {
        format!("{}{}{}", open, x, close)
    }

    println!("{}", enclose(14));
}

The compiler rejects this code with the error message can't capture dynamic environment in a fn item. Rust doesn't give nested functions closure semantics. If we want closure semantics, we must use a lambda form instead of a function definition:

Rust
fn main() {
    let open = '(';
    let close = ')';

    let enclose = |x: i32| -> String {
        format!("{}{}{}", open, x, close)
    };

    println!("{}", enclose(14));       // (14)
}

The Rust documentation doesn't use the term lambda. An anonymous function is just called a closure. A closure without parameters has an empty || for its parameter list. The arrow and return type are omitted when it doesn't return a value. The curly braces are optional when the body is a single statement or expression.

Closures and Memory

Closures may not seem relevant in a discussion of memory management in Rust. But they are included here because their interactions with the free variables from the surrounding context must satisfy Rust's borrow checker. In the closure above, open and close are implicitly borrowed. Suppose they are declared as mutable, and the closure's lifetime spans across their mutations, as in this code:

Rust
fn main() {
    let mut open = '(';
    let mut close = ')';

    let enclose = |x: i32| -> String {
        format!("{}{}{}", open, x, close)
    };

    println!("{}", enclose(14));

    open = '[';
    close = ']';

    println!("{}", enclose(15));
}

This code does not compile. The mutable owners in main and the borrowed references in enclose cannot coexist. There's not really an easy fix. If we have a closure that relies on mutable state, we should probably turn the closure into a stateful and mutable struct.

If the closure itself needs to mutate the free variables, then both the closure and the free variables must be declared mutable. This code creates a free string variable and a closure that appends to the string:

Rust
let mut danger = String::from("Danger");

let mut elevate = || {
    danger.push_str("!");
};

elevate();
elevate();
elevate();

println!("{}", danger);    // Danger!!!

This compiles and runs just fine even though there are two mutable references to the string. It works because the closure's reference becomes inactive after the elevate calls. The compiler only rejects the code if the references are active at the same time, as in this code:

Rust
let mut danger = String::from("Danger");

let mut elevate = || {
    danger.push_str("!");
};

println!("{}", danger);   // compile error
elevate();
elevate();
elevate();
println!("{}", danger);

The compiler rejects the first println!. It recognizes that a mutable reference has already been given to elevate and that the reference is still needed for the upcoming elevate calls.

We generally will not encounter mutability issues if we only define closures for their most common use case: passing anonymous functions to higher-order functions.

← Rc TypeHigher-order Functions →