Skip to content
Lesson 7 of 9

Step 1 of 6 · Reading · ~4 min

Learn

Smart Pointers and Memory

Closures: Capture, Fn, FnMut and FnOnce

By the end you can predict which closure trait a closure gets, accept closures in functions and struct fields, and return them.

In Rust Fundamentals you passed closures to map and filter, and in Traits and Generics you wrote bounds like T: Score. This lesson is about what a closure captures, because that decides which bound it satisfies.

What a closure captures

A closure stores the outside variables it uses, or references to them. The compiler picks the weakest way that works:

let name = String::from("ada");
let shout = || name.to_uppercase();   // only reads: borrows &name
println!("{} {}", shout(), shout());  // ADA ADA
println!("{}", name);                 // ada

let mut total = 0;
let mut add = |x: i32| total += x;    // writes: borrows &mut total
add(5);
add(7);
println!("{}", total);                // 12

Without mut on let add you get error[E0596]: cannot borrow add as mutable. The &mut borrow lasts until the closure's last call, as in Borrowing:

add(5);
println!("{}", total);   // error[E0502]: cannot borrow `total` as immutable
add(7);                  // closure used again below

move copies or moves, it does not change the trait

move makes the closure own its captures. For a Copy type that means a private copy:

let mut n = 0;
let mut bump = move || { n += 1; n };
println!("{} {}", bump(), bump());   // 1 2
println!("{}", n);                   // 0: the outside n is untouched

Fn, FnMut, FnOnce

The trait depends only on what the closure's body does with a capture:

The body...Implements
only reads itFn, FnMut, FnOnce
mutates itFnMut, FnOnce
moves it out (drop(tag), returning it)FnOnce only

A move closure that only reads is still Fn. Calling an FnOnce closure uses up the closure:

let tag = String::from("v1");
let finish = || drop(tag);
finish();
finish();   // error[E0382]: use of moved value: `finish`

Closures as parameters and fields

Calling an FnMut needs mut. Ask for the weakest bound you can live with, or callers are refused:

fn repeat<F: FnMut()>(times: u32, mut action: F) {
    for _ in 0..times { action(); }
}
fn twice<F: Fn()>(f: F) { f(); f(); }

let mut n = 0;
twice(|| n += 1);   // error[E0594]: cannot assign to `n`, as it is a
                    // captured variable in a `Fn` closure

A struct holds a closure through a generic parameter with the same bound. Call the field with parentheses, because self.action(x) looks for a method named action (E0599):

struct Every<F: FnMut(u32)> { step: u32, count: u32, action: F }

impl<F: FnMut(u32)> Every<F> {
    fn tick(&mut self) {
        self.count += 1;
        if self.count % self.step == 0 {
            (self.action)(self.count);
        }
    }
}
// action: |c| seen.push(c) with step 3, ticked 7 times: seen == [3, 6]

Returning closures, function pointers

Each closure has its own unnamed type, so return one with impl Fn, plus move so it does not borrow a parameter that dies with the call (E0597: k does not live long enough):

fn scaler(k: i32) -> impl Fn(i32) -> i32 { move |x| x * k }

Two closures are two types, so if d { |x| x * 2 } else { |x| x + 100 } is error[E0308]: if and else have incompatible types. Return Box<dyn Fn(i32) -> i32> and Box::new each branch; the mechanics of dyn come in Trait Objects.

A function pointer fn(i32) -> i32 accepts named functions and closures that capture nothing:

fn apply(f: fn(i32) -> i32, x: i32) -> i32 { f(x) }
apply(|x| x + 1, 4);        // 5
let k = 3;
apply(|x| x + k, 4);        // error[E0308]: mismatched types

Common mistakes

  • Expecting move to update the outside variable. After the bump example the outer n still prints 0.
  • Re-running an impure closure "to be safe". A closure with a hidden counter answers differently on every call, so later answers drift.
  • Forgetting mut on a binding or parameter holding an FnMut, or demanding Fn for a closure that mutates.
  • Writing self.f(n) for a closure field instead of (self.f)(n).

Your exercise

Finish get in the Memo wrapper so a repeated query is answered from its cache without calling the closure, while calls and hits stay accurate. The closure keeps a private counter, so one extra call changes every later answer.

Up nextModules and VisibilityModules and Threads

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…