DEV Community

Cover image for Build a function memoizer [Part-1]
Kailash Sankar
Kailash Sankar

Posted on

Build a function memoizer [Part-1]

The Problem Statement

Build a function which takes an input function and returns a new function which will memoize/cache the results.

I was asked this question in an interview and felt it's a good problem to solve and learn from. We'll focus on building something "good enough".

Rather than trying to solve for all the scenario's in one shot, it is better go incrementally especially during an interview. A solution that works for few scenarios is better than one which attempts to solve everything but doesn't run.

Let's start with an minimum viable option, assume a simple scenario: A function which performs some complex math operations on a set of input numbers

Breaking down the problem

  • We have to write a function which returns a function with caching
  • Where do we cache? In a closure
  • How do we cache? We need a unique key, we can form a key from all the input parameters. Since it's only numbers we can just join the values by '-'.

First step is to write a frame for our function


// takes and input function 
// returns a function wrapped in a closure
function memoizer(fn) {
  // capture all the input args
  return (...args) => {
    // call input function with args
    return fn(...args);
  };
}

// a test function
function add(a, b) {
  return a + b;
}

// call our memoizer
const memoAdd = memoizer(add);

console.log(memoAdd(1, 2)); // output: 3
console.log(memoAdd(2, 4)); // output: 6

Enter fullscreen mode Exit fullscreen mode

Next, a cache key maker

const generateCacheKey = (args) => args.join("-");

console.log(generateCacheKey([1, 2, 8, 44]));
// output: 1-2-8-44
Enter fullscreen mode Exit fullscreen mode

Next, we add add caching. Check if the key is in cache, if found then return from cache, otherwise call the function and cache result before returning it.

// build cache key
const generateCacheKey = (args) => args.join("-");

function memoizer(fn) {
  // cache store
  const resultsCache = {};

  // capture all the input args
  return (...args) => {
    const cacheKey = generateCacheKey(args);

    if (!(cacheKey in resultsCache)) {
      // cached value not found, call fn and cache result
      resultsCache[cacheKey] = fn(...args);
    }

    // return result from cache;
    return resultsCache[cacheKey];
  };
}

// we can use a counter to test if our cache is working
let count = 0;
function add(a, b) {
  count++;
  return a + b;
}

const memoAdd = memoizer(add);

const prettyPrint = (result) =>
  console.log(`result: ${result}, count: ${count}`);

prettyPrint(memoAdd(1, 2)); // result: 3, count: 1
prettyPrint(memoAdd(2, 4)); // result: 6, count: 2
prettyPrint(memoAdd(2, 4)); // result: 6, count: 2
prettyPrint(memoAdd(22, 33, 44)); // result: 55, count: 3
prettyPrint(memoAdd(1, 2)); // result: 3, count: 3
Enter fullscreen mode Exit fullscreen mode

The cache is working, the second time we called with args (2,4), the count remained the same proving that the value was returned from cache.

Now that we have a basic working implementation, it's time to list down the next set of features that we have to add and prioritise.

  • support for complex input params like Objects and Arrays
  • support for caching asynchronous functions like api calls
  • a clear cache option
  • support for a max cache limit, otherwise our cache will keep growing with variations in input
  • option to expire cached values based on time

I've listen these in order that I feel will add most value to the solution (if in an interview)

The following parts of this series will solve the listed items one by one.


Photo by Tim Mossholder on Unsplash

Top comments (1)

Collapse
 
mohsenalyafei profile image
Mohsen Alyafei

Thank you. This is helpful and useful.