We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Closures and Iterators step 27 of 28
The FnMut borrow gauntlet: a log you can read afterwards
Run a pipeline that records what it did, and return both the results and the log.
pub fn instrument(nums: Vec<i64>) -> (Vec<i64>, Vec<String>)
For each number, in input order:
-
if it is negative, log
"skip:{n}"and drop it; -
otherwise log
"keep:{n}"and emitn * 3.
Then append one final line to the log: "events:K", where K is the number
of lines logged before it.
[1, -2, 3] -> ([3, 9], ["keep:1", "skip:-2", "keep:3", "events:3"])
[] -> ([], ["events:0"])
[0] -> ([0], ["keep:0", "events:1"])
Zero is not negative, so it is kept.
The logging must be done by an FnMut closure that captures the log
vector, and the log must be readable afterwards. Sequencing those two
requirements is the entire exercise.
The error, and why it is where it is
The starter does the obvious thing and fails:
error[E0502]: cannot borrow `log` as immutable because it is also borrowed as mutable
The line it blames is let events = log.len(); — a read. Not a write.
Reading a vector you own, and being told you may not.
Here is the rule, and it is worth stating precisely because it is the one that catches everybody:
A closure that mutates a captured variable holds a
&mutto it, from the moment the closure is created until the closure’s last use. While that loan is live, the variable may not be accessed in any way — not read, not written, not even by its owner.
In the starter, record is created near the top and used again after the
log.len() call. So the loan spans the read, and the read is rejected. The
same code with the final record(..) deleted would compile, because
non-lexical lifetimes end a loan at its last use — the borrow checker does
not care about the closing brace, it cares about the last use.
This is a distilled version of the standard demonstration:
let mut count = 0;
let mut inc = || count += 1;
println!("{count}"); // E0502 — the read sits between creation and last use
inc();
Move the println! after inc() and it compiles. Nothing about the
closure changed. The shape of your control flow changed, and that is what
the borrow checker was reading all along.
The fix is to reorder, not to reach for a smart pointer
Two spellings, both good:
-
Let the closure die. Put the pipeline in a block that owns the closure;
at the closing brace the closure is dropped, the loan ends, and
logis yours again. Then read the length and push the final line directly. -
Just stop using it. NLL ends the loan at the closure’s last use, so if
the last thing you do with
recordis inside the chain, everything after thecollect()is already free. The explicit block is clearer, but not strictly required.
Either way, the final "events:K" line is pushed onto log directly,
not through the closure. That is the whole fix, and it is worth noticing how
small it is compared to how stuck it feels.
About RefCell
Most people reach for RefCell<Vec<String>> here within about ninety
seconds, and it does work. Let us be honest about when it is the right
answer.
It is not the right answer here. You have one closure, one piece of
state, single-threaded, and a natural point at which the closure is finished.
RefCell would move a check the compiler was doing for free at compile time
into a runtime check that can panic, and buy you nothing.
It is the right answer when:
-
the closure must be
Fn, notFnMut— some APIs demand it, and aFnclosure cannot hold a&mut. This is the common real case. -
two or more closures share the same state and are alive at once. Item
9.8 is exactly that: a
mapclosure and afilterclosure both appending to one log. No reordering can help, because both must exist simultaneously.RefCellis correct there and the runtime cost is a counter increment.
So: reorder when you can, RefCell when the ownership shape genuinely
demands it, and be able to say which one you are doing.
While you are there
A related lint you will meet the moment you start instrumenting pipelines:
error: using `map` over `inspect`
= note: `-D clippy::manual-inspect` implied by `-D warnings`
.map(|x| { log(x); x }) is .inspect(|x| log(x)). inspect exists exactly
for “look at each item as it goes past without changing it”, and it says so
at the call site. This problem’s side effects live in a filter predicate
rather than a map, so the lint does not apply — but inspect is the tool
when they live in the middle of a chain.
Grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.