Skip to content

← Closures and Iterators step 27 of 28

Medium Primitives

Read a log after its closure finishes

Process a list while recording one log entry for each input value. Return the output values and the completed log:

pub fn instrument(nums: Vec<i64>) -> (Vec<i64>, Vec<String>)

Visit values in input order:

  • For a negative n, append "skip:{n}" to the log and emit no value.
  • Otherwise, append "keep:{n}" and emit n * 3. Zero is kept.

After processing the entire input, append "events:K", where K counts the preceding log entries. It therefore equals the input length, including skipped values. This summary does not count itself. Preserve duplicates and use the exact strings shown, without spaces or newlines.

[1, -2, 3] -> ([3, 9], ["keep:1", "skip:-2", "keep:3", "events:3"])
[]         -> ([], ["events:0"])
[0]        -> ([0], ["keep:0", "events:1"])

Nonnegative inputs are at most i64::MAX / 3, so tripling them fits in i64. Negative values can include i64::MIN; they are logged and skipped before multiplication. The output and log fit in memory.

Use an FnMut closure that mutably borrows the log vector for the per-value entries. Finish using that closure before reading the log length, then push the summary directly onto the vector. This exercise practices sequencing ordinary borrows; keep the log as a Vec<String> without interior mutability.

Why the starter fails

Creating record borrows log mutably because its body calls log.push. The starter reads log.len() and then calls record again to write the summary. That later call keeps the mutable borrow active across the read, so the compiler reports E0502.

The conflict is between direct access to log and the borrow held by record. It does not prevent the closure itself from accessing the vector through that borrow. Nor does every FnMut closure borrow external state: a move closure can own its captured state instead.

End the borrow before reading

Replace the final record(...) call with log.push(...). The closure's last use is then in the pipeline consumed by collect(). After collection, the borrow checker can end that borrow and allow direct access to log.

You may also place the closure and pipeline in an inner block, as long as that block returns the collected output. This makes the separation visible, but an extra block is not required for this closure.

Here is a complete example of a captured borrow ending after the last call:

fn main() {
    let mut count = 0;
    let mut increment = || count += 1;
    increment();
    assert_eq!(count, 1);
}

Moving the assertion before increment() would put a shared read inside the mutable borrow's lifetime and fail to compile. The relevant boundary here is the closure's last use, not necessarily its enclosing closing brace.

Consume the pipeline before inspecting its effects

Iterator adapters such as filter are lazy. Constructing a pipeline alone does not visit the inputs or write their log entries. collect() drives this pipeline to completion, so the final length is read after all entries have been written.

Other designs may need shared access to mutable state. RefCell can support that within one thread by checking borrows at runtime; conflicting borrows panic unless handled through its fallible borrowing methods. It is an option, not an automatic requirement whenever two closures use the same state. Here, completing the closure's work first is enough.

Your solution must compile, follow the required closure structure, match the output and log, and pass Clippy with warnings treated as errors.