Skip to content

Closures

Rust’s closures are anonymous (not named) functions that capture their environment. That means that closures have the ability to access variables from the scope in which they are defined. Closures are first-class elements, meaning you can save in a variable or pass as arguments to other functions. Closures exist somewhere between functions and expressions, but syntactically look more like expressions. Closure expressions are represented in two parts; the parameters and a code block.

// Simplified closure expression
|| function_call()
// Multi-statement closure that takes a parameter x
|x| {
function_call();
second_function(x);
}

Closures are lumped in with the types here because they share similarities with data types. Closures can behave like functions in that they can be invoked with arguments, execute a block of code, and return values. Closures also behave like data types because they’re implemented as an anonymous type that represents the combination of their behavior and environment variables. Similarly, closures in Rust are considered “first-class citizens” which means that they can be assigned to variables, passed as arguments, and returned from functions. Closures are really just expressions that look a bit like function signatures. The closure syntax is defined by parameters wrapped in vertical bars || and a body that could be a function or a block of statements.

// A regular function that returns the double of its argument
fn testing(mut n: i32) -> i32 {
n += n;
n
}
fn main() {
let x = 10;
// The closure definition takes no arguments itself
let closure = || testing(x);
// The closure captures its environment so we dont need
// to pass x to the closure when calling it
println!("Closure: {}", closure());
}

This could also be expressed without the additional function.

fn main() {
let x = 10;
// The closure definition takes no arguments itself
// The closure is stored in a variable (called closure)
let closure = || x * 2;
// The closure captures its environment so we dont need
// to pass x to the closure when calling it
println!("Closure: {}", closure());
}

Closures infer types because they are generally not exposed as APIs but rather used internally. It is possible to annotate types for clarity, but often not necessary. With full type annotations closures look almost identical to functions.

fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
// The closure can be simplified without type annotations
let add_one_v3 = |x| x + 1;

The compiler infers one concrete type per parameter within scope. This means that the closure will work the first time with any type, but if we call it later in the same scope the compiler will error.

let not_ok = |x| x + 1;
let a: i32 = 12;
let b: usize = 23;
// Illegal!
let val = not_ok(a) + not_ok(b);

Closure References & Ownership

Closures use the requirements of their content blocks to determine whether an immutable borrow, mutable borrow, or ownership is required. In this example the push() method for Vector types requires a mutable borrow so the closure requires a mutable borrow. This means we cannot place a println!() statement between the closure definition and the closure call because Rust does not allow more than one mutable borrow at a time. We can capture a mutable borrow after the mutable borrow ends (the last closure call).

fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
let mut borrows_mutably = || list.push(7);
// Illegal call!
// println!("After defining closure: {:?}", list);
borrows_mutably();
println!("After calling closure: {:?}", list);
}

Use the move keyword to force the closure to take ownership. This may be helpful when passing the data to a new thread.

use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
thread::spawn(move || println!("From thread: {:?}", list))
.join()
.unwrap();
}

The way a closure captures and handles values from the environment affects which traits the closure implements, and traits are how functions and structs can specify what kinds of closures they can use. Closures will automatically implement one, two, or all three of these Fn traits. Functions can also implement any or all of these traits. Traits can be applied in an additive fashion, depending on how the closure’s body handles the values:

  1. FnOnce applies to closures that can be called once. All closures implement at least this trait, because all closures can be called. A closure that moves captured values out of its body will only implement FnOnce and none of the other Fn traits, because it can only be called once.
  2. FnMut applies to closures that don’t move captured values out of their body, but that might mutate the captured values. These closures can be called more than once.
  3. Fn applies to closures that don’t move captured values out of their body and that don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.

One common function that takes closure expressions is the unwrap_or_else() method defined in the standard library for the Option<T> (enum) type. This method handles types that return Option<T> types. The trait bound specified on the generic type F is FnOnce() -> T, which means F must be able to be called once, take no arguments, and return a T. Using FnOnce in the trait bound expresses the constraint that unwrap_or_else is only going to call f at most one time. The method checks the instanced Option<T> type and returns the value of Some if it finds one. If unwrap_or_else finds None it performs the closure expression once. Because FnOnce is the most generic type of closure, this method takes pretty much all closure statements. Lets look at the function definition from source.

#[inline]
#[track_caller]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T,
{
match self {
Some(x) => x,
None => f(),
}
}

Its possible to call this function with something like the following example. In this example the build() function returns a Result<T, E> type. The return value can be handled in a handful of ways, from using a match block, to using a simple unwrap(). We’ve decided to use unwrap_or_else() which takes a single argument of type F of type FnOnce(). The FnOnce() trait is implemented by closures that consume their captured values. a closure and returns result is, which means they can return Some or None.

// unwrap_or_else() takes a closure expression
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Error parsing arguments: {err}");
process::exit(1);
});

Or perhaps,

// map() takes a closure expression
let sv: Vec<u32> = s.chars().map(|x| x.to_digit(16).unwrap_or(0)).collect();

The unwrap_or_else() method requires a closure with the trait bound FnOnce() because we only need to find out if the passed Option<T> is Some. The sort_by_key() method requires a closure with the FnMut trait because it calls the closure once for each element in the argument. The book provides an example of sorting a collection of structs.

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut rectangles = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
rectangles.sort_by_key(|r| r.width);
}

In my own experiments I found that the closure’s argument requires a reference if the object being sorted is not a tuple or other compound type.

let mut v = vec![1, 3, 5, 4, 2];
v.sort_by_key(|&x| x); // The argument is required to be a reference
// v.sort(); // A more appropriate method
let mut t = vec![(1, 11), (3, 31), (5, 51), (4, 41) , (2, 21)];
t.sort_by_key(|x| x.0);

This is not really an issue because the proper method to use in this case would be sort(). This is only being noted because there is no indication of this behavior in the sort_by_key() definition.

pub fn sort_by_key<K, F>(&mut self, f: F)
where
F: FnMut(&T) -> K,
K: Ord,