cf.completefrontend

Write the codeMedium

Write memoizeOne(fn): cache only the most recent call, and recompute when the arguments change.

Before you start

What's memoization?

Memoization means saving a function's result for the arguments it was called with, so that calling it again with the same arguments returns the saved result instead of running the function again. The saved results are the function's cache. When a call finds its result there, that's a cache hit; when it doesn't, it's a miss, and the function runs and its result is saved. Memoizing trades memory for time, so it pays off for functions that are slow and get called with the same arguments again and again.

It's like writing the answer to a long sum in the margin, so the next time someone asks, you read it off instead of working it out. Take countPrimes(n), which counts the prime numbers below n and is slow when n is large. Here's what a memoized version does over four calls, next to a lighter variant that remembers only its most recent call:

CallMemoized: remembers every callRemembers only the last call
countPrimes(1000000)Miss: runs, returns 78498, and saves it.Miss: runs, returns 78498, and saves it.
countPrimes(1000000)Hit: returns the saved 78498 without running.Hit: returns the saved 78498 without running.
countPrimes(2000000)Miss: runs, returns 148933, and saves it too.Miss: runs, returns 148933, and saves it in place of 78498.
countPrimes(1000000)Hit: 78498 is still saved.Miss: only the result for 2000000 is saved, so it runs again.

Every saved result, with the arguments it was saved for, stays in memory until the cache lets go of it, and a cache that never does grows with every new argument. Memoize helpers deal with that in different ways: some let you clear the cache, some cap its size and drop the least recently used result when it's full, and the variant in the last column keeps only one. That one suits a value recomputed again and again from inputs that rarely change. React's useMemo works that way: it keeps the value from the last render and recomputes it only when one of its dependencies has changed.

A cache finds results by a key worked out from the arguments. Lodash's _.memoize uses the first argument as the key unless you pass a resolver, a function that turns the arguments into a key. Without one, a memoized power(base, exponent) called as power(2, 3) and then power(2, 10) returns 8 both times: both calls have the key 2. Lodash compares keys the way a Map does, so an object matches only the very same object: a different object with the same contents is a miss, and changing an object's contents doesn't make it miss. Other helpers build the key from every argument.

Only memoize pure functions: ones whose result depends on nothing but their arguments, and that don't change anything outside themselves. A hit skips the function entirely. If getPrice(id) reads a price that has since changed, the memoized version keeps returning the old one, and a function that logs something or sends a request doesn't do it on a hit.

For an async function, the result is a promise, so the promise is what gets saved. A second call with the same arguments gets the same promise, even while it's still pending, so two parts of a page asking for the same user share one request. A promise that rejects would be saved too, and every later call would get the same failure, unless the cache forgets keys whose promise rejects.

Besides useMemo, you meet memoization in React's memo, which skips re-rendering a component whose props haven't changed, in selector libraries such as Reselect, and in recursive algorithms that would otherwise solve the same smaller problem many times.

memoizeOne(fn) returns a new function that calls fn with the same this and arguments and returns fn's result. It remembers the most recent call: its this, its arguments, and the result.

When a call has the same this and the same arguments as the previous one (as many arguments, each equal by Object.is), return the remembered result without calling fn. Otherwise call fn, and remember this call in place of the previous one.

If fn throws, keep remembering the previous call.

Write this
function memoizeOne(fn) {
  // your code
}
 
// const square = memoizeOne((n) => { console.log("computing"); return n * n; });
// square(4); // logs "computing", returns 16
// square(4); // returns 16 from the cache
// square(5); // logs "computing", returns 25
// square(4); // logs "computing" again: only the last call is cached

The answer

Write it yourself first. Then compare it with a reference solution and the checks it passes.

Checking your sign-in…