Skip to content

← Ownership II: Borrowing and the Borrow Checker step 21 of 24

Medium Primitives

Closures hold their borrows

Modify a vector by applying operations in order, and return one log entry for each operation.

pub fn apply_ops(xs: &mut Vec<i64>, ops: &[String]) -> Vec<String>
Operation Effect
push <n> Append the parsed i64 value.
pop Remove the last element if there is one.
double Multiply each element by two.

Recognize pop and double exactly. For a push, require the exact prefix push followed by a signed decimal integer accepted by parse::<i64>(): an optional + or -, then one or more ASCII digits, with a value in range. Leading zeros are valid. Do not trim whitespace, accept underscores or change letter case. Thus push +03 is valid, while push 3, push 3 and Push 3 are errors. An out-of-range integer is also an error.

After a recognized operation, append "<op> -> len=<new length>". For an invalid operation, append "<op> -> error" and leave the vector unchanged. In both cases, <op> is the original string, including its spaces.

Popping an empty vector succeeds and logs pop -> len=0. Empty operations leave the vector unchanged and return an empty log. Inputs ensure that every multiplication performed by double fits in i64; arithmetic overflow is not an additional operation-error case.

For example, starting with an empty vector and applying ["pop", "push 3", "double", "push x"] leaves [6] and returns:

pop -> len=0
push 3 -> len=1
double -> len=1
push x -> error

Why the starter's capture conflicts

The starter defines let mut push = |n: i64| xs.push(n);. This closure captures access to xs so it can mutate the vector. The loop calls the closure, then also uses xs directly to pop, double or read its length. Because the closure may be called again on a later iteration, the exclusive borrow overlaps those direct accesses. Rust reports E0501.

A captured borrow does not necessarily last until the end of the closure binding's scope. When no later use needs the borrow, it can end earlier:

fn main() {
    let mut values = vec![1];
    let mut push = |n| values.push(n);
    push(2);
    // No more uses of push, so its borrow no longer blocks this access.
    values.push(3);
    assert_eq!(values, [1, 2, 3]);
}

The starter cannot use that reasoning between iterations: its closure still needs the vector for future calls.

Pass the vector into the operation

Give the closure a vector parameter instead of capturing the vector. Here is a smaller example of the same pattern:

fn main() {
    let push = |values: &mut Vec<i64>, number| values.push(number);
    let mut values = vec![1];
    push(&mut values, 2);
    assert_eq!(values.len(), 2);
    push(&mut values, 3);
    assert_eq!(values, [1, 2, 3]);
}

This closure neither stores nor returns the borrowed vector. Each call can therefore finish its borrow before the next direct access. A function or closure that returns a borrowed value could keep a borrow needed for longer; passing a parameter alone does not guarantee that every borrow ends at return.

In your solution, an operation helper can accept &mut Vec<i64> and &str and return a bool indicating whether it recognized a valid operation. After the call, use that result and xs.len() to build the log entry. A named helper function or inline loop logic is also valid.

What move changes

Adding move is not a repair for the starter's competing accesses. It would move the &mut Vec<i64> reference into the closure, leaving the local xs binding unavailable for the loop's other branches. It does not move the caller's vector allocation or prevent the caller from using its vector once this function returns.

Capture ownership also differs from the callable traits. The starter's closure needs mutable access when called, so its binding is mut. The parameter-based closure above captures nothing and can mutate its argument without a mutable closure binding. A move closure may still implement Fn if calling it only reads its captured state.

Separate fields can be borrowed separately

In edition 2021 and later, closure capture can be precise enough to borrow one field while leaving another available:

struct Pair {
    values: Vec<i64>,
    count: usize,
}

fn main() {
    let mut pair = Pair { values: vec![1], count: 7 };
    let read_count = || pair.count;
    pair.values.push(2);
    assert_eq!(read_count(), 7);
    assert_eq!(pair.values, [1, 2]);
}

This does not mean every field access results in a separate capture. Using the whole value or an array index, for example, can require a broader capture. The starter's closure and direct accesses all need the same vector, so field precision does not resolve that conflict.