kota's memex

Closures are anonymous functions that you can save in a variable or pass as arguments to other functions. Unlike functions, closures can capture values from the scope in which they're defined.

let expensive_closure = |num: u32| -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

Optional type annotations

The type annotations are optional for a closure. Here's a reference of a function vs a few different ways to define a closure:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

Once you've used a closure the types you've used it with are set in stone. This following code will not compile:

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);

Threads

The new thread might finish before the rest of the main thread finishes, or the main thread might finish first.

The move keyword converts any variables captured by reference or mutable reference to variables captured by value.

let data = vec![1, 2, 3];
let closure = move || println!("captured {data:?} by value");
// data is no longer available, it is owned by the closure

Fn Traits

A closure body can do any of the following: move a captured value out of the closure, mutate the captured value, neither move nor mutate the value, or capture nothing from the environment to begin with.

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, 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 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 mutliple times concurrently.

For example take the implementation of the unwrap_or_else() method:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

The generic type parameter F is bound upon FnOnce() -> T which means a closure that will only be called once and must return T which is the option type above. In the body of the function, if the Option is Some then f will not be called. If it is instead, None, then f will be called exactly once. This is enforced by the compiler. Because all closures implement FnOnce, unwrap_or_else accepts all three kinds of closures and is as flexible as it can be.