Skip to the code
cf.

Memoize

MediumFunctions

Write memoize(fn). It returns a function that calls fn with the same arguments and returns its result, but remembers every result: a later call with the same arguments returns the remembered result without calling fn again.

Two calls count as the same when they have the same number of arguments and each argument matches the one in the same position, compared the way a Map compares keys: 1 and "1" are different, two objects with the same contents are different, and an object is always the same as itself.

Results of undefined are remembered too. Each memoized function has its own cache.

const add = memoize((a, b) => {
  console.log("adding", a, b);
  return a + b;
});
add(1, 2);   // logs "adding 1 2", returns 3
add(1, 2);   // returns 3, logs nothing
add(1, 3);   // logs "adding 1 3": different arguments
add("1", 2); // logs "adding 1 2" again and returns "12": "1" isn't 1

What the tests check

  1. calls fn once for repeated arguments
    const fn = spy((a, b) => a + b);
    const add = memoize(fn);
    expect(add(1, 2)).toBe(3);
    expect(add(1, 2)).toBe(3);
    expect(add(1, 2)).toBe(3);
    expect(fn).toHaveBeenCalledTimes(1);
    
  2. uses every argument, not only the first
    const fn = spy((a, b) => a + b);
    const add = memoize(fn);
    expect(add(1, 2)).toBe(3);
    expect(add(1, 3)).toBe(4);
    expect(fn).toHaveBeenCalledTimes(2);
    
  3. tells 1 and "1" apart
    const fn = spy((x) => typeof x);
    const kind = memoize(fn);
    expect(kind(1)).toBe("number");
    expect(kind("1")).toBe("string");
    
  4. doesn't confuse ("a,b") with ("a", "b")
    const fn = spy((...args) => args.length);
    const count = memoize(fn);
    expect(count("a,b")).toBe(1);
    expect(count("a", "b")).toBe(2);
    
  5. compares objects by identity, not by contents
    const fn = spy((user) => user.name);
    const getName = memoize(fn);
    const ada = { name: "Ada" };
    getName(ada);
    getName(ada);
    expect(fn).toHaveBeenCalledTimes(1);
    getName({ name: "Ada" });
    expect(fn).toHaveBeenCalledTimes(2);
    
  6. tells f(1) and f(1, undefined) apart
    const fn = spy((...args) => args.length);
    const count = memoize(fn);
    expect(count(1)).toBe(1);
    expect(count(1, undefined)).toBe(2);
    expect(count()).toBe(0);
    
  7. remembers undefined results
    const fn = spy(() => undefined);
    const nothing = memoize(fn);
    nothing("x");
    nothing("x");
    expect(fn).toHaveBeenCalledTimes(1);
    
  8. each memoized function has its own cache
    const double = memoize((x) => x * 2);
    const square = memoize((x) => x * x);
    expect(double(5)).toBe(10);
    expect(square(5)).toBe(25);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).