Caching Decorator

Volodymyr Yepishev - Jun 6 '22 - - Dev Community

Let's imagine we've got a class that does some heavy computations, since my imagination is limited, I will suggest a class that does counting (you can introduce something cooler in the comments, so I can stop embarassing myself):

class CountdownCalculator {
  public countDown(n: number): number {
    let count = 0;
    while (count < n) {
      count += 1;
    }
    return count;
  }
}
const countdownCalculator = new CountdownCalculator();
Enter fullscreen mode Exit fullscreen mode

So obviously the bigger number we pass, the more time it will be working. At some point we might want to memoize its results, so it case the method receives a number it has calculated previously, it simply returns the result right away.

Let's call the method a couple of times and see how it goes:

for (let x = 0; x++ <= 1;) {
  const start = performance.now();
  const result = countdownCalculator.countDown(500000000);
  const end = performance.now();

  console.log(`Result is: ${result}. Execution time of the first invocation: ${end - start} ms`);
}

// [LOG]: "Result is: 500000000. Execution time of the first invocation: 157.19999992847443 ms" 
// [LOG]: "Result is: 500000000. Execution time of the first invocation: 156 ms"
Enter fullscreen mode Exit fullscreen mode

We could modify the method/class itself so it memoizes the arguments, or we could use the cool decorator feature typescript offers us and create a decorator for that:

function memoize(
  target: unknown,
  propertyKey: string,
  descriptor: PropertyDescriptor
): void {
  const cachedResults = new Map<number, number>();

  const originalMethod = descriptor.value;

  descriptor.value = function (...args: unknown[]) {
    const num = args[0] as number;
    if (cachedResults.has(num)) {
      return cachedResults.get(num)
    }
    const result = originalMethod.apply(this, args);
    cachedResults.set(num, result);
    return result;
  };
}
Enter fullscreen mode Exit fullscreen mode

The implementation is naive and simplified, i.e. it doesn't deal with such problems as multiple arguments or objects passed as arguments. But anyway.

Now we can decorate our goofy method with the caching decorator and see it in action (the whole code is available in the sandbox btw):

function memoize(
  target: unknown,
  propertyKey: string,
  descriptor: PropertyDescriptor
): void {
  const cachedResults = new Map<number, number>();

  const originalMethod = descriptor.value;

  descriptor.value = function (...args: unknown[]) {
    const num = args[0] as number;
    if (cachedResults.has(num)) {
      return cachedResults.get(num)
    }
    const result = originalMethod.apply(this, args);
    cachedResults.set(num, result);
    return result;
  };
}

class CountdownCalculator {
  @memoize
  public countDown(n: number): number {
    let count = 0;
    while (count < n) {
      count += 1;
    }
    return count;
  }
}

const countdownCalculator = new CountdownCalculator();

for (let x = 0; x++ <= 1;) {
  const start = performance.now();
  const result = countdownCalculator.countDown(500000000);
  const end = performance.now();

  console.log(`Result is: ${result}. Execution time of the first invocation: ${end - start} ms`);
}

// [LOG]: "Result is: 500000000. Execution time of the first invocation: 156.40000009536743 ms" 
// [LOG]: "Result is: 500000000. Execution time of the first invocation: 0 ms" 
Enter fullscreen mode Exit fullscreen mode

Cool, eh? :D

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .