Skip to content

← Errors Are Values step 6 of 24

Medium Primitives

Mutating an Option in place

Run a tiny memoising cache and report, for every operation, what the cache was holding — plus a final count of how many times the expensive computation actually ran.

pub fn run(ops: Vec<String>) -> Vec<i32>

Internally you keep cache: Option<i32> and computations: i32. Each op pushes exactly one number onto the output; after the last op, push computations.

op behaviour pushes
"get" return the cached value, computing it only if absent the value
"clear" empty the cache the value that was there, or -1
"set:N" put N in the cache the value that was there, or -1
anything else ignored -1

The computation, when it runs, is computations += 1 followed by computations * 10. So the first computed value is 10, the second 20, the third 30. That numbering is what makes “did it recompute?” an assertion rather than a guess.

Worked example — ["get", "get", "clear", "get"]:

  • get → cache empty, compute (computations = 1) → push 10
  • get → cache hit, no computation → push 10
  • clear → cache held 10 → push 10, cache now empty
  • get → cache empty, compute (computations = 2) → push 20
  • end → push 2

giving [10, 10, 10, 20, 2].

Why the starter is wrong

The starter uses Option::insert, and that is a genuine and very common confusion:

  • insert(v) sets the slot to v unconditionally, overwriting whatever was there, and returns &mut T pointing at the new value.
  • get_or_insert(v) sets it only if it was None, and returns &mut T to whatever is now there.
  • get_or_insert_with(f) is the same, but only calls f when it is actually going to insert — this is the unwrap_or_else distinction again, and it is the whole reason a cache is a cache.

Its "clear" and "set:N" arms have the other bug: they reassign the slot (cache = None) and so throw away the old value they were supposed to report.

The four moves

let mut slot: Option<i32> = Some(1);

slot.take()                 // -> Some(1), slot is now None
slot.replace(7)             // -> the old value as an Option, slot is now Some(7)
slot.insert(9)              // -> &mut 9, unconditionally overwrites
slot.get_or_insert_with(f)  // -> &mut T, calls f() only if it was None

Option::take is the one to remember. It is std::mem::replace(&mut slot, None) with a nicer name, and it is the canonical way out of E0507 when your T is not Copy:

struct Node { next: Option<Box<Node>> }

// E0507: cannot move out of `node.next` which is behind a mutable reference
let tail = node.next;

// fine: `take` swaps None in, so `node` is still complete afterwards
let tail = node.next.take();

That pattern is how linked structures, builders and state machines get restructured in Rust. Beginners who never learn it reach for .clone() and then wonder why their tree costs a full copy to walk.

Because this problem’s T is i32 (which is Copy), you get to practise the move without fighting the borrow checker. The methods are identical for non-Copy types — that is the point of doing it here first.

The &mut return, and the NLL stumble

get_or_insert_with returns &mut T, not T. That reference borrows the whole Option for as long as you hold it, so this fails:

let slot = cache.get_or_insert_with(|| compute());
let hit = cache.is_some();      // E0502: `cache` is already mutably borrowed
out.push(*slot);

The fix is almost always to stop holding the reference: *cache .get_or_insert_with(…) copies the value out and the borrow ends at the end of that statement. If T were not Copy you would .clone() it, or restructure so the borrow does not have to outlive the statement.

E0499 is the sibling error — two &mut to the same thing at once, which is what you get if you call two of these methods and keep both results.

Notes

  • take_if(p) (1.80) takes the value only if the predicate says so; get_or_insert_default() (1.83) is get_or_insert_with(Default::default).
  • mem_replace_option_with_some is the clippy lint for std::mem::replace(&mut opt, Some(v)) — say opt.replace(v).
  • Option<Option<T>> (clippy::option_option) almost always means a design mistake: nobody can remember which layer means what. Use a three-variant enum with named variants instead.
  • -1 is a sentinel for “the cache was empty”. Sentinels are exactly what Option exists to replace — it is used here only because the harness speaks JSON arrays of integers, and it is worth noticing that you had to give one up to talk across that boundary.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…