CS3984 Computer Systems in Rust



Closures

Closures are anonymous functions that can capture values from the scope in which they are defined.

Closures are named by assigning them to variables, and have syntax that resemble functions:

fn main() {
    // These closures are all equivalent
    let add_one   = |x| x + 1;
    let add_one_2 = |x| { x + 1 };
    let add_one_3 = |x: i32| -> i32 { x + 1 };

    // Closures that take a single tuple
    let take_pair =   |(x, y): (i32, i32)| -> i32 { x + y };
    let take_pair_2 = |pair: (i32, i32)| -> i32 { pair.0 + pair.1 };

    // Closure with multiple parameters
    let take_two = |x: i32, y: i32| -> i32 { x + y };
}


Capturing State

Closures can capture state:

fn main() {
    let list = vec![1, 2, 3];
    println!("Before definition: {list:?}");

    // The variable `list` is borrowed by the closure
    let closure = || println!("During call: {:?}", list);

    println!("Before call: {list:?}");
    closure();
    println!("After call: {list:?}");
}


Fn Traits

Closures automatically implement up to the least restrictive type of one of the following traits (in order of most to least restrictive) depending on the values they capture:

FnOnce

  • The closure might move ownership of captured values.

FnMut

  • The closure does not move ownership of captured values.
  • The closure might mutate captured values.

Fn

  • The closure does not capture any values or captures them immutably without moving ownership.
  1. Types that implement Fn also implement FnMut and FnOnce.
  2. Types that implement FnMut also implement FnOnce.

FnOnce

  • FnOnce closures can be called once (all closures can be called).
  • Closures that only implement FnOnce can be called at most once because they obtain ownership of their values.
fn main() {
    // Closures that mutably capture values implement `Fn`
    let list = vec![1, 2, 3];
    let move_ownership = || drop(list);

    move_ownership(); // `list` is dropped here
    // move_ownership();  The closure cannot be called again
}


FnMut

  • FnMut closures can be called multiple times because they do not obtain ownership of any values.
  • Closures that only implement FnMut (and FnOnce by extension) cannot be called from multiple threads because they mutate state without synchronization.
fn main() {
    let mut list = vec![1, 2, 3];

    let mut mutate_list = |n: i32| {
      list.push(n);  // `list` is mutated here
    };

    mutate_list(4);
    mutate_list(5);
    println!("{list:?}");
}


Fn

  • Fn closures can be called multiple times because they do not obtain ownership of any values.
  • Fn closures can be called across multiple threads because they do not mutate their environment.
fn main() {
    // Closures that do not capture values implement `Fn`
    let add_one = |x: i32| x + 1;

    // Closures that immutably capture values implement `Fn`
    let list = vec![1, 2, 3];
    let only_borrows = || println!("From closure: {list:?}");
}


Function Pointers

Functions that are assigned to variables become function pointers:

fn add_one(x: usize) -> usize {
    x + 1
}

fn main() {
    let fn_ptr: fn(usize) -> usize = add_one;
}


Safe function pointers (safe as in safe rust) implement Fn, FnMut, and FnOnce.

Case Study

The Option::unwrap_or_else method can be called with closures that are Fn, FnMut, and FnOnce.

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(),
        }
    }
}