Proposal: Function static variables and blocks

A proposal to add a static keyword for variable declaration in functions.
Proposal repository: yehoryatskevych/proposal-function-static-variables (github.com)

The problem

Allocation in JavaScript can hardly be called "fast". JavaScript is a great language, and its features significantly reduce development time, making it the best language for prototyping and fast development. However, it is still an interpreted language, which takes a lot of time for native calls and allocations. Therefore, it would be great to have more options for manual code optimization.

Let's consider an example with a function that just creates a 3D vector using some extra parameter:

function task(i) {
    new Vector3(i + 1.13, i + 5.231, i + 7.1247);
}

Every time this function is called, JavaScript allocates a new object for the Vector3 instance. We can easily measure the execution time of this function by calling it 10^8 times:

const ITERATIONS_NUM = Math.pow(10, 8);

console.time('task');
for (let i = 0; i < ITERATIONS_NUM; ++i) {
    task(i);
}
console.timeEnd('task');

The result:

task: 920.739ms

I don't think that's a good result. So let's test again, but move the vector allocation to the global scope:

let vec = new Vector3(0, 0, 0);

function task(i) {
    vec.set(i + 1.13, i + 5.231, i + 7.1247);
}

const ITERATIONS_NUM = Math.pow(10, 8);

console.time('task');
for (let i = 0; i < ITERATIONS_NUM; ++i) {
    task(i);
}
console.timeEnd('task');

The result:

task: 78.667ms

Looks much better, even though we are setting the value every call. But is it reasonable to put all variables into the global context? I don't think so, especially if your goal is to write high-performance code with many functions that need to be optimized. So you can use closure with an Immediately Invoked Function Expression (IIFE), like this:

const task = (() => {
    let vec = new Vector(0, 0, 0);
    return function task(i) {
        vec.set(i + 1.13, i + 5.231, i + 7.1247);
    }
})();

However, we are immediately confronted with the fact that we can't use function declarations, and hoisting no longer works for us. So we have to declare these functions before we use them, and we already get restrictions that slow down development speed and interfere with the desired structuring of the code, especially when there are a lot of such functions. Using such optimizations in methods becomes almost impossible.

The solution

One solution to this problem could be local static variables, as implemented in some other C-like low-level languages. These variables would allow us to optimize memory allocation and function execution time by allocating and initializing the variables only once after the first function call. Subsequent function calls would use the already allocated variable with the last value. It's similar to global variables that can only be accessed from the specific function in which they are declared.

Here is an example of how it could be used:

function task(i) {
    static let vec = new Vector(0, 0, 0);
    vec.set(i + 1.13, i + 5.231, i + 7.1247);
}

and a more complex example of how it should work:

function func() {
    static let isFirstCall = true;
    static let counter = 0;

    if (isFirstCall) {
        isFirstCall = false;
        console.log('Static variables initialized!');
    }

    console.log("Counter:", counter);

    counter++;
}

func(); // OUT: "Static variables initialized!", "Counter: 0"
func(); // OUT: "Counter: 1"
func(); // OUT: "Counter: 2"

This code proposal introduces a highly useful feature for functions with high execution rates and constants that require complex operations for initialization. It allows functions to store their own state for subsequent executions and remain independent of the global context. With this feature, functions can efficiently handle their own state persistence and eliminate the need for global variables or external dependencies.

To make it more consistent with the current static implementation and more flexible it also can be added with the support of class-static-block proposal.

function func() {
    static let counter = 0;

    static {
        console.log('Static variables initialized!');
    };

    console.log("Counter:", counter);

    counter++;
}

func(); // OUT: "Static variables initialized!", "Counter: 0"
func(); // OUT: "Counter: 1"
func(); // OUT: "Counter: 2"

Additional benefits

In addition to helping with optimization, it can also be useful to store some function states independently from global scope for example for util/helper functions that need to have some state and pre-allocation, refactoring is also made easier due to the elimination of the need to move variables and "helpers" for the "helper".

function funtionWithComplexPreallocation(value) {
    static const magic = 6816;
    static const magicOffset = 285615417;
    static const magicRatio = complexOperation(magic);
    return magicOffset + (value * magicRatio);   
}

or

function funtionWithComplexPreallocation(value) {
    static const magic = 6816;
    static const magicOffset = 285615417;
    static let magicRatio;

    static {
        for (let i = 0; i < 1024; ++i) {
            magicRatio = Math.sqrt(magicRatio) * magic;
        }
    }

    return magicOffset + (value * magicRatio);   
}

Previous discussion: C-style static variables

3 Likes

Note that you could also write:

function task (i) {
  task.vec ??= new Vector(0, 0, 0);
  task.vec.set(i + 1.13, i + 5.231, i + 7.1247);
}

which should probably be almost as fast as your IIFE example

I've sometimes found myself slightly wanting a feature like this, but mostly for use-cases like "I'm making an id-retrieving function that keeps an internal state of the last ID used in order to decide the next one", or that sort of thing.

Though, for me, do expressions (if they ever come) would be a good enough solution. Not sure that it would satisfy you though, since you're saying that you'd still like function hosting and what-not, which do expressions can't provide, and they're technically not much better than IIFEs, just less clunky.

No, it is not reasonable to put those variables in the global scope. However, you really should use modules, and it is totally reasonable to declare such variables in the module scope. Just don't export them.

const declarations are still hoisted, though yes, you need to initialise them before you use them. This is usually not a problem in well-structured code that only calls functions from the script entrypoint, not during module initialisation.
And really, I would not want to rely on hoisting for initialising static variables, having the order of evaluation of static blocks depend on the order of calls to the functions seems like a nightmare to me.

Any thoughts how we can combine two approaches with my Lazy inlined constants ?

Also propose this.

Let's compare various solutions.

Simplest

// Free function case
let fn_calls = 0;
const fn = () {
  if (fn_calls++) { return; }
};

// Class Method case
let A_fn_calls = 0;
class A {
  fn() {
      if (A_fn_calls++) { return; }
  }
}

Cons

  • Parent scope is polluted.
  • If you want do it better, you need to name it a long name to indicates "this variable is dedicated to fn"!

Function as namespace

// Free function case
const fn = () {
  if (fn.calls++) { return; }
}
fn.calls = 0;

// Class method case
class A {
  fn() {
      if (A.prototype.fn.calls++) { return; }
  }
}
A.prototype.fn.calls = 0;

Cons

  • Not TypeScript friendly.
  • Not tree shaking friendly.
  • Not JS engine friendly.
  • You don't want to write the A.prototype.fn, do you?

IIFE

// Free function case
const fn = (() => {
  let calls = 0;
  return () => {
    if (fn.calls++) { return; }
  };
})();

// Class method case
class A {
   // Cons:
   // - enumerable: true
   //  - (new A().fn !== new A().fn)
   //  - No longer able to use "class method decorator".
   fn = (() => {
     let calls = 0;
     return () => {
        // .....
     };
   })();
}

Cons

  • Verbose.
  • Can not be applied to class method, given you don't want it be a "dummy function property".

Best(This Proposal)

// Free function case
const fn = () => {
  static let calls = 0;
  if (fn.calls++) { return; }
};

// Class method case
class A {
  fn() {
      static let calls = 0;
      if (fn.calls++) { return; }
  }

  // You can even ...
  get surprise() {
    // readonly, expensive, cache
    static let cache = {};
    cache.x = this.x;
    return cache;
  }
}

Cons

  • Easy to read.
  • No pollution.
  • TypeScript friendly.
  • Tree shaking friendly.
  • JS engine friendly.
  • Easy to transform.
  • No compatibility problem.
  • Support all form of "function-like" language constructions.
  • You can type less words.

Pros

  • No

Some questions:

  1. What is the difference between static let and static var? Seems let and var here are meaningless.
  2. What happen if we use static let name = "abc" ?

And this proposal is different from IIFE because static variables are not fully enclosed.

  1. What happen if we use static let name = "abc" ?

Static function variable is a separate variable:

  • declare at the point the enclosing function/method/accessor declares

  • can only be accessed within the function

  • finishe its initialization before first access.

    If its initializer does not depend on any function local variable, it can be done as soon.

{ // containing scope
  /// ...
  function fn(p) {
    static let name = "abc" + p; // Referencing function-local variable? strange but allowed
    static let name_2 = "abc";
  }
  // ...
}

should be desugar to:

{ // containing scope
  // ...
  var $name;
  var $name_initialized = false;

  var $name2 = "abc";

  function fn(p) {
    if (!$name_initialized) {
      $name_initialized = true;
      $name = "abc" + p;
    }
    // `name_2`'s initialization has been hoisted
  }
  // ...
}

I would think as OP said it should be consistent with the current static implementation.
Please see the example at MDN's Static_initialization_blocks

Neither let nor var is used. And in fact, if you use static let... in Class, it is an error.

static name = "abc" + p; won't works in Class, so I don't think it should work in Function.

Also, the static variables should also be properties of the function.

My concern of a "name" static variable because it could be misleading for debugging tools.
Try to run the following in node.js

> class A {
static name="B"}
> A

it returns [class B] { name: 'B' } rather than [class A] { name: 'B' }