An analogy between Rust's closures and structs

Rust's closures may use funny syntax and may seem a bit magical, but they're actually little more than syntax sugar for a struct with a method. This conceptual correspondence might help you to transfer your intuition about structs to closures.

A simple analogy🔗

A closure such as...

let z: i32 = 42;
let closure = |x: i32| -> i32 { x * z };
assert_eq!(closure(2), 84);

could be approximated as...

struct MyClosure1 {
    z: i32,
}
impl MyClosure1 {
    pub fn call(&self, x: i32) -> i32 { x * self.z }
}

let z: i32 = 42;
let closure = MyClosure1 { z };
assert_eq!(closure.call(2), 84);

Notably, we create fields for the 'captured' local variables, using self to access them within the call method that represents the closure's body.

Although we now need to write closure.call(2) instead of closure(2) to call our 'closure', this starts to paint a picture. It does, however, leave some details behind.

Capturing local scope🔗

Using a Copy type such as i32 was perhaps not the best example.

Let's take a look at some closures that actually hold on to the local 'scope'.

Immutable references🔗

From the land of contrived examples, I pluck out this closure which will return the sum of some numbers, multiplied by the provided factor. The closure needs to borrow the Vec of numbers so that it can sum them on demand.

let numbers: Vec<i32> = vec![1, 2, 3];

let sum_times = |factor: i32| -> i32 {
    numbers.iter().sum::<i32>() * factor
};

assert_eq!(sum_times(2), 12);

Expressed in our struct analogy, this would look something like this:

struct MyClosure2<'a> {
    numbers: &'a Vec<i32>,
}
impl<'a> MyClosure2<'a> {
    pub fn call(&self, factor: i32) -> i32 { self.numbers.iter().sum::<i32>() * factor }
}

let numbers: Vec<i32> = vec![1, 2, 3];
let sum_times = MyClosure2 { numbers: &numbers };

assert_eq!(sum_times.call(2), 12);

Note that the struct is now parameterised with a lifetime (because it holds a reference to numbers), but is otherwise much the same. This boilerplate really makes you grateful for the syntax sugar that is the || {} syntax for closures...

Mutable references🔗

Much the same, except you sprinkle mut in the places that make sense. Also note that call now takes &mut self, so in effect would need to be represented by our equivalent of FnMut (let's get to that later).

Before:

let mut numbers: Vec<i32> = Vec::new();

let mut push_if_even = |x: i32| {
    if x % 2 == 0 {
        numbers.push(x);
    }
};

push_if_even(2);
push_if_even(3);
push_if_even(4);

assert_eq!(numbers, vec![2, 4]);

After:

struct MyClosure3<'a> {
    numbers: &'a mut Vec<i32>,
}
impl<'a> MyClosure3<'a> {
    pub fn call(&mut self, x: i32) {
        if x % 2 == 0 {
            self.numbers.push(x);
        }
    }
}

let mut numbers: Vec<i32> = Vec::new();
let mut push_if_even = MyClosure3 { numbers: &mut numbers };

push_if_even.call(2);
push_if_even.call(3);
push_if_even.call(4);

assert_eq!(numbers, vec![2, 4]);

Note that this example requires non-lexical lifetimes: push_if_even has to die before we get to the assertion, since otherwise push_if_even's mutable reference would prevent us from being able to read numbers (due to borrow checking rules). If you wish, try adding push_if_even.call(5); after the assertion; in either case (closure-style or struct-style), the example will no longer compile :-).

The move keyword🔗

So far, the examples haven't moved anything into the closure (i32 is Copy and doesn't need moving; the other examples have shown references). Moving things into the closure is a necessity when we want the closure to outlive its scope — often I find myself needing that when spawning threads or async tasks.

For completeness, let's show an example here — using the example of returning the closure to justify moving things into it.

fn make_closure(numbers: Vec<i32>) -> impl Fn(usize) -> i32 {
    move |idx: usize| {
        numbers[idx]
    }
}

let closure = make_closure(vec![10, 40, 90]);

assert_eq!(closure(1), 40);

(Please forgive the pointlessness of the closure body!)

Note how we have to use impl Fn(usize) -> i32 as the return type — closures don't have a type that you can explicitly write, so we either have to do this or resort to dynamic dispatch with a Box<dyn Fn(usize) -> i32>.

Carrying this example over to our analogy, we have:

struct MyClosure4 {
    numbers: Vec<i32>,
}
impl MyClosure4 {
    pub fn call(&self, idx: usize) -> i32 {
        self.numbers[idx]
    }
}

fn make_closure(numbers: Vec<i32>) -> MyClosure4 {
    MyClosure4 { numbers }
}

let closure = make_closure(vec![10, 40, 90]);

assert_eq!(closure.call(1), 40);

In short: things being moved into the 'closure' is the same as a struct field that isn't a reference!

The Fn, FnMut and FnOnce traits🔗

This analogy is perhaps most useful once you consider the Fn, FnMut and FnOnce traits. What exactly do they mean?

Let's make our own de-sugared traits for those. Note that, for simplicity, we'll restrict ourselves to single-parameter functions.

pub trait MyFn<A, R> {
    fn call(&self, arg: A) -> R;
}

pub trait MyFnMut<A, R> {
    fn call(&mut self, arg: A) -> R;
}

pub trait MyFnOnce<A, R> {
    fn call(self, arg: A) -> R;
}

The key message to take home here is that the only thing that changes is whether the self-parameter is &self, &mut self or self!

We then would want to implement these traits for our ad-hoc struct-closures — for example, using the last example:

impl MyFn for MyClosure4 {
    pub fn call(&self, idx: usize) -> i32 {
        self.numbers[idx]
    }
}

We can now use impl MyFn<usize, i32> in lieu of impl Fn(usize) -> i32 in cases where we want to accept an arbitrary kind of closure. We can also use Box<MyFn<usize, i32>> in lieu of Box<Fn(usize) -> i32>!

Implied traits🔗

One thing worth noting is that, in plain Rust, an Fn is also an FnMut and an FnOnce. (Further, an FnMut is also an FnOnce.) We could make this easier in our system by automatically implementing those, as follows...

impl<A, R, T: MyFn<A, R>> MyFnMut<A, R> for T {
    fn call(&mut self, arg: A) -> R {
        MyFn::<A, R>::call(self, arg)
    }
}

impl<A, R, T: MyFnMut<A, R>> MyFnOnce<A, R> for T {
    fn call(mut self, arg: A) -> R {
        MyFnMut::<A, R>::call(&mut self, arg)
    }
}

(In practice, this might get annoying since all the methods have the same name, leading to the unfortunate need to disambiguate which call we mean (as you can even see in the implementation!). Perhaps they should be called call, call_ref and call_mut or something?)

Under the bonnet: plain Rust's traits🔗

Out of interest, if you went to look at the definition of Fn, FnMut and FnOnce, you'd find something not entirely dissimilar to what we've written above:

pub trait Fn<Args>: FnMut<Args> {
    /// Performs the call operation.
    #[unstable(feature = "fn_traits", issue = "29625")]
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

pub trait FnMut<Args>: FnOnce<Args> {
    /// Performs the call operation.
    #[unstable(feature = "fn_traits", issue = "29625")]
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait FnOnce<Args> {
    /// The returned type after the call operator is used.
    #[lang = "fn_once_output"]
    #[stable(feature = "fn_once_output", since = "1.12.0")]
    type Output;

    /// Performs the call operation.
    #[unstable(feature = "fn_traits", issue = "29625")]
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

Some of that is magic; magic which is out of scope for this document which was intended to make things simpler, but this is just to show that the approximation we're talking about in this post isn't that poor.

Summary🔗

If you imagine closures as just being structs with a trait implementation, you won't go far wrong.

If you'd appreciate a source file with tests demonstrating the code samples included in this post, click here.

Otherwise, this table summarises the analogy:

Plain Rust closures'Struct-closures'

Fn(A) -> R

FnMut(A) -> R

FnOnce(A) -> R

pub trait MyFn<A, R> {
    fn call(&self, arg: A) -> R;
}

pub trait MyFnMut<A, R> {
    fn call(&mut self, arg: A) -> R;
}

pub trait MyFnOnce<A, R> {
    fn call(self, arg: A) -> R;
}
closure(42);
closure.call(42);
let numbers: Vec<i32> = vec![1, 2, 3];

let sum_times = |factor: i32| -> i32 {
    numbers.iter().sum::<i32>() * factor
};
struct MyClosure2<'a> {
    numbers: &'a Vec<i32>,
}
impl<'a> MyClosure2<'a> {
    pub fn call(&self, factor: i32) -> i32 { self.numbers.iter().sum::<i32>() * factor }
}

let numbers: Vec<i32> = vec![1, 2, 3];
let sum_times = MyClosure2 { numbers: &numbers };
let mut numbers: Vec<i32> = Vec::new();

let mut push_if_even = |x: i32| {
    if x % 2 == 0 {
        numbers.push(x);
    }
};
struct MyClosure3<'a> {
    numbers: &'a mut Vec<i32>,
}
impl<'a> MyFnMut for MyClosure3<'a> {
    pub fn call(&mut self, x: i32) {
        if x % 2 == 0 {
            self.numbers.push(x);
        }
    }
}

let mut numbers: Vec<i32> = Vec::new();
let mut push_if_even = MyClosure3 { numbers: &mut numbers };
fn make_closure(numbers: Vec<i32>) -> impl Fn(usize) -> i32 {
    move |idx: usize| {
        numbers[idx]
    }
}
struct MyClosure4 {
    numbers: Vec<i32>,
}
impl MyFnOnce for MyClosure4 {
    pub fn call(&self, idx: usize) -> i32 {
        self.numbers[idx]
    }
}

fn make_closure(numbers: Vec<i32>) -> MyClosure4 {
    MyClosure4 { numbers }
}