Build a function memoizer [Part-2]
Kailash Sankar
Posted on July 25, 2020
Continuing where we left off in the last part, we'll start by adding support for complex input parameters like Objects and Arrays.
The easiest way to create a unique key for complex params would be to JSON.stringify
the input params. MDN has note that stringify does not guarantee any specific order but it is good enough for now. There are npm modules which can ensure a consistent hash.
Update the cache key generating function
// build cache key
const generateCacheKey = (args) => args.map((x) => JSON.stringify(x)).join("-");
// test
console.log(generateCacheKey([3, { x: "hello", y: "world" }, [81, "on3"], 22]));
// output: 3-{"x":"hello","y":"world"}-[81,"on3"]-22
Test if caching is working for array/object params
// new test function with inputs - array, number and object
let count = 0;
function calc(values, multiplier, labels) {
count++;
const total = values.reduce((acc, x) => x + acc, 0) * multiplier;
return `${labels.text} => ${total}`;
}
prettyPrint(memoizedCalc([10, 2], 2, { text: "A" }));
// result: A => 24, count: 1
prettyPrint(memoizedCalc([1], 1, { text: "B" }));
// result: B => 1, count: 2
prettyPrint(memoizedCalc([10, 2], 2, { text: "A" }));
// result: A => 24, count: 2
The count remained the same, so our caching now supports complex inputs.
Let's see what happens when we use the memoizer for an asynchronous function.
// function to call mock api
let count = 0;
async function getTodo(id) {
count++;
return fetch(
`https://jsonplaceholder.typicode.com/todos/${id}`
).then((res) => res.json());
}
const memoizedGetTodo = memoizer(getTodo);
// call async functions
(async function () {
prettyPrint(await memoizedGetTodo(1));
// output: result: {...}, count: 1
prettyPrint(await memoizedGetTodo(2));
// output: result: {...}, count: 2
prettyPrint(await memoizedGetTodo(1));
// output: result: {...}, count: 2
})();
It's working for async! The memoizer we wrote in Part-1 already supports async methods which return a promise.
How? On the first call, the code will cache an unresolved promise and immediately return a reference to it.
If cache is dumped, you will see something like
'1': Promise { <pending> }
The caller awaits for resolve, when it triggers the promise in cache becomes resolved and execution continues.
'1': Promise { { userId: 1, id: 1 ....} }
Now, we have in cache, a resolved promise which will then be returned whenever we see the same input parameters.
The next item in our list is a clear function which will allow the caller to clear the cache in closure. We have to re-write a bit of the memoizer as below to include the clear action.
function memoizer(fn) {
// cache store
let resultsCache = {};
// memoized wrapper function
// capture all the input args
function memoized(...args) {
const cacheKey = generateCacheKey(args);
if (!(cacheKey in resultsCache)) {
// cached value not found, call fn and cache result
resultsCache[cacheKey] = fn(...args);
}
//console.log("cache", resultsCache);
// return result from cache;
return resultsCache[cacheKey];
}
// clear cache
memoized.clearCache = () => {
resultsCache = {};
};
return memoized;
}
Let's see if it works as expected
prettyPrint(await memoizedGetTodo(1));
// output: result: {...}, count: 1
prettyPrint(await memoizedGetTodo(2));
// output: result: {...}, count: 2
prettyPrint(await memoizedGetTodo(1));
// result: {...}, count: 2
memoizedGetTodo.clearCache(); // clear the results cache
prettyPrint(await memoizedGetTodo(1));
// result: {...}, count: 3
Clearing the cache resulted in the last call hitting the base function and incrementing the counter to 3.
The next part of series will add support for setting cache size limit.
Posted on July 25, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.