CouchCoder

Search


Latest Posts


Recent Comments


December 2017
M T W T F S S
« Apr    
 123
45678910
11121314151617
18192021222324
25262728293031
CouchCoder

Using a Memoized Decorator to Cache Computed Properties

Create a simple decorator that when applied to a computed property, will cache the result of the first read, and use that instead in subsequent calls.

MerottMerott

In this blog post, we’re going to create a simple decorator that when applied to a computed property, will memoize the result of the first read, and use that instead in subsequent calls. But first, a little bit of background…

Background

One of the new features of ES6 that I really like is the new support for property getters and setters. You could always write them, but the syntax was a lot more verbose with Object.defineProperty. With a getter, you can provide a computed value without method calls. Think Array.prototype.length, a very simple computed property.

Here’s a MyString class with a computed reversed property:

class MyString {
  constructor(value) {
    this.value = value;
  }
  
  get reversed() {
    // using esrever for unicode-aware string reversal
    return esrever.reverse(this.value);
  }
}

let str = new MyString('foo 𝌆 bar');
console.log(str.reversed);  // logs rab 𝌆 oof

As noted in the comment, we’re using Esrever to reverse the string without breaking unicode characters. While effective, that could be be a performance issue if the property is being accessed too frequently. To address that problem, there is a common pattern to cache/memoize the reversed string, so that we only have to call esrever.reverse once.

class MyString {
  constructor(value) {
    this.value = value;
  }
  
  get reversed() {
    return typeof this._reversed !== 'undefined' ? 
        this._reversed :
        this._reversed = esrever.reverse(this.value);
  }
}

let str = new MyString("foo 𝌆 bar");

console.log(str.reversed);  // logs rab 𝌆 oof
console.log(str.reversed);  // logs rab 𝌆 oof
console.log(str.reversed);  // logs rab 𝌆 oof

The above code will only call esrever.reverse once. It caches the result into _reversed, and returns that on subsequent reads. Neat.

Better with a Decorator

Here comes the interesting part: we can simplify this pattern using decorators. Decorators are proposed for ES2016 (more commonly referred to as ES7), and they’re also already available in TypeScript. So, let’s do this.

We’ll call this decorator Memoized, and we’re going to be applying it to our getter like this:

class MyString {
  value:string;
	
  constructor(value:string) {
    this.value = value;
  }
  
  @Memoized
  get reversed():string {
    return esrever.reverse(this.value);
  }
}

To implement an accessor decorator, we’re going to need to define a function with the following call signature:

function Memoized(target:any, propertyKey:string, descriptor:PropertyDescriptor)

What we’re interested in is the 3rd argument: the descriptor. If you’re not familiar with property descriptors already, I suggest you read this.

What we want to do is really simple. We want to replace the existing getter function with another function that uses the original getter, but also caches the result:

function Memoized(target:any, propertyKey:string, descriptor:PropertyDescriptor) {
  let value:any;
  let originalGet = descriptor.get;

  descriptor.get = function() {
    if(!this.hasOwnProperty('__memoized__')) {
      Object.defineProperty(this, '__memoized__', { value: new Map() });
    }

    return this.__memoized__.has(propertyKey) ?
        this.__memoized__.get(propertyKey) :
        (() => {
          const value = originalGet.call(this);
          this.__memoized__.set(propertyKey, value);
          return value;
        })();
  };
}

We’re using a non-enumerable, read-only Map property named __memoized__ to hold our memoized values. This property is created the first time a memoized getter of the class is called. We check if __memoized__ already has the property that we’re interested in, and if it does, we return that. If it does not, we call the original getter function to get the value for the first time and cache it for subsequent calls.

Note that we’re using the traditional function expression to override descriptor.get, as opposed to the new arrow syntax, in order to retain the correct this context, which is passed on to originalGet via .call.

That’s it! Here is the complete code:

// ============ Memoized.decorator.ts ============ //
export function Memoized(
    target:any, propertyKey:string, descriptor:PropertyDescriptor
  ) {
  let value:any;
  let originalGet = descriptor.get;

  descriptor.get = function() {
    if(!this.hasOwnProperty('__memoized__')) {
      Object.defineProperty(this, '__memoized__', { value: new Map() });
    }

    return this.__memoized__.has(propertyKey) ?
        this.__memoized__.get(propertyKey) :
        (() => {
          const value = originalGet.call(this);
          this.__memoized__.set(propertyKey, value);
          return value;
        })();
  };
}

// ============ MyString.ts ============ //
import { Memoized } from './Memoized.decorator'

class MyString {
  value:string;
	
  constructor(value:string) {
    this.value = value;
  }
  
  @Memoized
  get reversed():string {
    return esrever.reverse(this.value);
  }
}

I'm a web developer who enjoys coding on the couch as much as I enjoy developing applications as a profession. I owe much of my success as a developer to the very people who blog and share, and this is my attempt to give something back to the community. I'm an advocate of writing clean, concise, well-structured code, and I also love to experiment with new technologies, trying to get my hands dirty with anything and everything.

Comments 2
  • Zak
    Posted on

    Zak Zak

    Reply Author

    Nice post, @decorators sure are a nice way to add discreet functionality to methods. If only their definitions weren’t so hard to read.

    Is there any reason other than style to put all the memoized properties in a single Map?

    I tried the following and it seemed to work fine.

    export function Memoized(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      const memoizedPropertyKey = `__memoized__${propertyKey}`;
      const originalGet         = descriptor.get;
    
      descriptor.get = function () {
        if (!this.hasOwnProperty(memoizedPropertyKey)) {
          Object.defineProperty(this, memoizedPropertyKey, {value: originalGet.call(this)});
        }
    
        return this[memoizedPropertyKey];
      };
    }
    

    Test case

    class MyString {
      value: string;
    
      constructor(value: string) {
        this.value = value;
      }
    
      @Memoized
      get reversed(): string {
        return this.value.split('').reverse().join('');
      }
    
      @Memoized
      get doubled(): string {
        return this.value + this.value;
      }
    
    }
    
    describe('@memoize', () => {
    
      it('should shows that state is not shared between instances', () => {
    
        const myString = new MyString('hello world');
    
        expect(myString.reversed).toEqual('dlrow olleh');
        expect(myString.reversed).toEqual('dlrow olleh');
        expect(myString.doubled).not.toEqual('dlrow olleh');
        expect(myString.doubled).toEqual('hello worldhello world');
    
        expect(new MyString('foo').doubled).not.toEqual('hello worldhello world');
    
      });
    
    });
    
    

    • Merott
      Posted on

      Merott Merott

      Reply Author

      Hey Zak!

      No real reason there. I just wanted to avoid adding __memoized__-prefixed properties for every memoized property of the class. But then again, it’s not a big deal, and you’re highly unlikely to have conflicts anyway.

      Also, I could have just used an object literal instead of a Map object. One could argue that using an object literal might lead to unexpected behaviour if a memoized property name matches the name of a method on Object.prototype, but the likelihood is again low, and you could work around it by using a prefix on memoized property names.

      Thanks for reading!