For AI agents: the complete documentation index is available at /tc39-atlas/en/llms.txt, the full documentation bundle is available at /tc39-atlas/en/llms-full.txt, and this page is available as Markdown at /tc39-atlas/en/proposals/stage/1/proposal-function-memo.md.
  • English
  • Function Memoization S1

    Proposal details
    Proposal overview

    The proposal introduces Function.prototype.memo, which creates a new function that caches the result of each unique set of arguments and returns the cached result on subsequent identical calls. It also explores a function-decorator form, @Function.memo, and an optional user-supplied Map-like cache for memory-constrained memoization. Open design questions include how cache keys and garbage collection should work.

    Note

    The README below comes from the upstream repository and may contain outdated stage or status metadata. Use the proposal details above as the current source of truth.

    Function.prototype.memo for JavaScript

    ECMAScript Stage-1 Proposal.

    Champions: Hemanth HM; J. S. Choi.

    Rationale

    Function memoization is a common technique that caches the results of function calls and returns the cached results when the same inputs occur again. These are useful for:

    Memoization is useful, common, but annoying to write. We propose exploring the addition of a memoization API to the JavaScript language.

    If this proposal is approved for Stage 1, then we would explore various directions for the API’s design. We would also assemble as many real-world use cases as possible and shape our design to fulfill them.

    In addition, if both proposal-policy-map-set and this proposal are approved for Stage 1, then we would explore how memoized functions could use these data structures to control their caches’ memory usage.

    Description

    The Function.prototype.memo method would create a new function that calls the original function at most once for each tuple of given arguments. Any subsequent calls to the new function with identical arguments would return the result of the first call with those arguments.

    function f (x) { console.log(x); return x * 2; }
    
    const fMemo = f.memo();
    fMemo(3); // Prints 3 and returns 6.
    fMemo(3); // Does not print anything. Returns 6.
    fMemo(2); // Prints 2 and returns 4.
    fMemo(2); // Does not print anything. Returns 4.
    fMemo(3); // Does not print anything. Returns 6.

    Additionally, we may also add a function-decorator version: @Function.memo. This would make it easier to apply memoization to function declarations:

    @Function.memo
    function f (x) { console.log(x); return x * 2; }

    Either version would work with recursive functions:

    // Version with prototype method:
    const getFibonacci = (function (n) {
      if (n < 2) {
        return n;
      } else {
        return getFibonacci(n - 1) +
          getFibonacci(n - 2);
      }
    }).memo();
    console.log(getFibonacci(100));
    
    // Version with function decorator:
    @Function.memo
    function getFibonacci (n) {
      if (n < 2) {
        return n;
      } else {
        return getFibonacci(n - 1) +
          getFibonacci(n - 2);
      }
    }
    console.log(getFibonacci(100));

    Result caches

    The developer would be able to pass an optional cache argument. This argument must be a Map-like object with .has, .get, and .set methods. In particular, there is a proposal for Map-like objects with cache-replacement policies like LRUMap, which would allow developers to easily specify that the memoized function use a memory-constrained cache.

    There are at least two possible ways we could design the cache parameter; see Issue 3 and Issue 4.

    Tuple keys?

    A: We could use tuples as the cache’s keys. Each tuple represents a function call to the memoized function, and the tuple would be of the form #[thisVal, newTargetVal, ...args].

    Object values would be replaced by symbols that uniquely identify that object. (Tuples cannot directly contain objects. The memoized function’s closure would close over an internal WeakMap that maps objects to their symbols.)

    const cache = new LRUMap(256);
    const f = (function f (arg0) { return this.x + arg0; }).memo(cache);
    const o0 = { x: 'a' }, o1 = { x: 'b' };
    f.call(o0, 0); // Returns 'a0'.
    f.call(o1, 1); // Returns 'b1'.

    Now cache would be LRUMap(2) { #[s0, undefined, 0] ⇒ 'a0', #[s1, undefined, 1] ⇒ 'b1' }, where s0 and s1 are unique symbols. f’s closure would internally close over a WeakMap { o0 ⇒ s0, o1 ⇒ s1 }.

    The default behavior of memo (i.e., without giving a cache argument) is uncertain (see Issue 3). It probably would be simply be an unbounded ordinary Map. (WeakMaps cannot contain tuples as their keys.)

    Composite keys?

    B: Another choice for cache’s keys is composite keys. Each composite key represents a function call to the memoized function, and the composite key would be of the form compositeKey(thisVal, newTargetVal, ...args).

    const cache = new LRUMap(256);
    const f = (function f (arg0) { return this.x + arg0; }).memo(cache);
    const o0 = { x: 'a' }, o1 = { x: 'b' };
    f.call(o0, 0); // Returns 'a0'.
    f.call(o1, 1); // Returns 'b1'.

    Now cache would be LRUMap(2) { compositeKey(o0, undefined, 0) ⇒ 'a0', compositeKey(o1, undefined, 1) ⇒ 'b1' }.

    The default behavior of memo (i.e., without giving a cache argument) is uncertain (see Issue 3). It probably would simply be a WeakMap, which would be able to contain composite keys as their keys.

    Unresolved questions

    Issue 2:

    Should memo be a prototype method, a static function, a function decorator, or multiple things?

    Issue 3:

    How should cache garbage collection work? (Using WeakMaps for the caches would be ideal…except that WeakMaps do not support primitives as keys.)

    Should we just use Maps and make the developer manage the cache memory themselves? (See also LRUMap and LFUMap.)

    There is also the compositeKeys proposal.

    Issue 4:

    If we go with a Map cache, how should we structure the cache? For example, we could use a tree of Maps, or we could use argument-tuples as keys in one Map.

    Issue 5:

    How should function calls be considered “equivalent”? How are values compared (e.g., with SameValue like === or SameValueZero like Map.get)? Are the this-binding receiver and the new.target value also used in comparison?

    Precedents