Skip to content

← Smart Pointers and Interior Mutability step 20 of 26

Medium Primitives

OnceCell: write-once interior mutability

Cache a sorted copy of the input so that several queries can reuse it. This is memoization: storing a computation's result instead of repeating it.

pub fn cached_stats(data: Vec<i64>, queries: Vec<String>) -> (Vec<i64>, usize)

Fix the starter's Stats type using OnceCell<Vec<i64>> for the sorted cache. Keep its query methods taking &self and use Cell<usize> to count how often the cache initializer runs.

Return the answers in query order and the sort count. Query names are case-sensitive:

Query Answer for nonempty data Initializes the cache?
median sorted[len / 2], the upper middle element for even lengths Yes
p90 sorted[min(len * 9 / 10, len - 1)] using integer division Yes
min sorted[0] Yes
count The input length No
Anything else -1 No

For empty data, the four recognized queries return 0; unknown queries still return -1. Calling median, p90, or min initializes the cache even when the input is empty.

The sort count is 1 if any query needs the cache, and 0 otherwise. Repeated queries reuse the same sorted vector. Each call to cached_stats creates its own Stats value and cache.

For example, data [8, 2, 6, 4] and queries ["count", "median", "p90", "min", "median"] return ([4, 6, 8, 2, 6], 1). The sorted copy is [2, 4, 6, 8]. This exercise uses an upper median, not the average of the two middle values. Its p90 formula is also an explicit indexing convention; do not substitute another percentile definition.

Why initialize through a shared reference?

Sorting a cached copy does not change the input or the statistics it describes. Methods can therefore offer a shared-reference API while updating internal cache state. This is a useful application of interior mutability.

Keep both the sort and the counter increment inside the cache initializer. A count query can read data.len() directly, without allocating or sorting.

Why the RefCell starter fails

The last line of sorted() borrows the RefCell, obtains a reference into its contents, and tries to return that reference. The temporary Ref guard is dropped when the method returns. Rust rejects the escaping reference with E0515 because it cannot outlive that guard.

RefCell can support caching with a different API: callers could retain a mapped guard, run a closure while borrowed, or receive a copied scalar result. It does not support the starter's attempt to return a plain slice reference after dropping the guard. OnceCell fits the intended signature.

What OnceCell provides

get_or_init returns a reference tied to the borrow of the cell. Through shared access, callers cannot replace an initialized value, so this needs no runtime borrow counter. It still checks whether initialization is needed.

get() reads without initializing. set(value) returns Err(value) if already initialized. An initializer that unwinds leaves the cell empty; trying to initialize the same cell recursively is an error. Reading other fields from the initializer is fine.

Write-once applies to shared access, not to every possible use of the cell. With exclusive access, get_mut() allows mutation and take() removes the value. A cache can therefore be invalidated by a method taking &mut self. Existing borrows prevent that exclusive access while they are still needed.

Related cell types

Cell<usize> suits the counter because it reads and replaces a small Copy value without a borrow guard. Increment it with update or set(get() + 1).

LazyCell stores its initializer at construction. OnceCell lets a method supply one later, which makes borrowing self.data straightforward here. Neither is Sync; they can be used in thread-local storage but cannot directly serve as shared immutable statics. Use the synchronized counterparts, OnceLock and LazyLock, when sharing between threads, subject to their type bounds.

A passing submission must compile, pass the tests, and pass clippy -D warnings.

See the OnceCell documentation for the shared-access and exclusive-access methods.