This is a spinoff from Method decorator that allows to execute a decorated method only once Typescript.
I have some get
methods on a typescript class that are computationally expensive. For some of them, I can be sure that the result is the same every time - it does not change based on the state of the class instance. However, the method is called many times on a single instance in the code. In that regard, it makes sense to only actually run calculations the first time, and not on subsequent times. For example:
class Cell {
angle: number;
count: number;
private _cosine?: number;
constructor(angle) {
this.angle = angle;
this.count = 0;
}
get cosine() {
if (this.count) return this._cosine;
this._cosine = Math.cos(this.angle);
this.count++;
return this._cosine;
}
}
const cells = Array.from({ length: 100 }).map(
(_, i) => new Cell(i * 180 * Math.PI)
);
cells.forEach((cell) => {
for (i = 0; i < 100; i++) {
const cosine = cell.cosine;
}
});
The first time cell.cosine
is accessed, it actually runs the computationally heavy code and assigns the result to the private property _cosine
. Every subsequent time, it simply returns that value. If you run this code, you'll see that the .count
of any cell
is only 1, even though cell.cosine
is accessed 100 times per instance.
The disadvantage of this is that for every property I want this once-only logic to apply to, I need to declare a private _property
, and include that little bit of logic. There was a great answer to the question of decorating a method such that it only runs once. User plumbn adapted this to work with getters. However, that answer doesn't work for this case of a getter/accessor, because a getter needs to return a value every time it is called.
Is it possible to write a decorator for get methods such that it only truly runs the first time it is called (and returns the value), and on subsequent times, it returns the initially calculated value without running calculations again?
You may ask "why not just assign the value in the constructor?" That's a great question. If you can, you should, as its a huge improvement in performance, as this jsbench shows. However, aside from being ugly, there are certain cases where that won't work. For example
class Cell {
angle: position;
count: number;
constructor(position) {
this.position = position;
this.count = 0;
}
get neighbors() {
let neighbors = [];
for (let j = -1; j <= 1; j++) {
for (let i = -1; i <= 1; i++) {
neighbors.push(
new Cell([x + i, y + j]),
);
}
}
return neighbors;
}
}
In this case, calling a cell.neighbor
method spawns 8 new Cells. If I went to assign this.neighbors
in the constructor, then each newly spawned Cell would call this.neighbors
in its constructor, and we've got an infinite loop. In this case, cell.neighbors
returns the same value every time it is accessed, but its a waste to run that code every time its called. I can use a if (this._neighbors) { return this._neighbors } else { this._neighbors = run_my_code(); return this._neighbors }
pattern, but it seems more elegant to capture this behavior in a decorator. The jsbench I posted above shows a slight improvement in performance when doing this (not as good as declaring it in the constructor, but still an improvement).
I believe we can compute the value the first time and store it, and then retrieve it on subsequent calls using reflect-metadata
as follows.
import "reflect-metadata";
const metadataKey = Symbol("initialized");
function once(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const getter = descriptor.get!;
descriptor.get = function () {
const val = Reflect.getMetadata(metadataKey, target, propertyKey);
if (val) {
return val;
}
const newValue = getter.apply(this);
Reflect.defineMetadata(metadataKey, newValue, target, propertyKey);
return newValue;
};
}
A working example can be found here.
Big thanks to OP for helping to get the kinks worked out.