"static <expr>" syntax

TL;DR

function fn(){
  let a = static {x: 0}
  a.x++
  return a
}

console.assert(fn().x == 1)
console.assert(fn().x == 2)
console.assert(fn().x == 3)
const o = fn()
console.assert(fn() == fn() && fn() == o)
o.x = 99
console.assert(fn().x == 100)

Give additional functionality to the static keyword, to statically allocate an object
I.e, the expression following the static keyword would only be evaluated once, and never again, even if it is found inside a function or loop. The static expression would then return the same value every time.

Example use cases

Consider the current solution to a hypothetical problem:

const privateSymbol = Symbol('hidden')

function processObject(obj){
    if(obj[privateSymbol]) return obj[privateSymbol]
    //...
    // perform some processing
    //...
    obj[privateSymbol] = processedValue
    return processedValue
}

Despite this, privateSymbol remains exposed. We could create an IIFE or some other kind of closure,

var processObject
{
    const privateSymbol = Symbol('hidden')

    processObject = function(obj){
        if(obj[privateSymbol]) return obj[privateSymbol]
        //...
        // perform some processing
        //...
        obj[privateSymbol] = processedValue
        return processedValue
    }
}

However, it would make more sense if we didn't have to define the symbol outside the function

function processObject(obj){
    const privateSymbol = Symbol('hidden')

    // .....
}

Unfortunately, now, our private symbol will be different with each call to the function.

In this proposal, the static keyword would be able to turn an expression inside a function (or loop, or anything else really) into a statically evaluted one,

function processObject(obj){
    // This expression will evaluate to the same each time
    const privateSymbol = static Symbol('hidden')


    if(obj[privateSymbol]) return obj[privateSymbol]
    //...
    // perform some processing
    //...
    obj[privateSymbol] = processedValue
    return processedValue
}

Other use cases

In react, it's not uncommon to declare styles inline, directly inside a component, e.g

const MyComponent = () => {
    return <div style={ {color: 'red', fontSize: 30, cursor: 'pointer'} }>
        Hello!
    </div>
}

React would then have to parse the style object and turn it into a style declaration every time the component is rerendered, which is a lot of work, and it perhaps one of react's biggest performance downfall, especially noticable when using large complex apps like Discord

The static keyword would allow a cache-like optimization, where an object could be recognised, and would not require re-parsing, e.g

function styleObjectToCss(obj){
  const cachedCss = static Symbol('cachedCss')

  // If the object has been seen before, use its cached value
  if(obj[cachedCss]) return obj[cachedCss]
  
  // Otherwise, perform expensive conversion
  const cssText = ...(obj)
  obj[cachedCss] = cssText
  return cssText
}

And our component would look like this

const MyComponent = () => {
    return <div style={ static {color: 'red', fontSize: 30, cursor: 'pointer'} }>
        Hello!
    </div>
}

static would allow the same object to be passed each time, essentially allowing react to easily cache its css representation and only process our object once.

Other use cases

There plenty other use cases that would be made simpler with the static keyword, here are a few examples:

  • sha256 uses a precomputed table of fractional digits of sqrt(2)

  • Functions can now be organized inside other functions without worrying about capturing or performance overhead (e.g when an exported function thing is a wrapper to the real function called _thing)

  • Node uses objects as option parameters absolutely everywhere. Using static would allocate the object on the heap, but would only do so once during the entire duration of the program.

Important noteworthy point: Since each static object is allocated once only, it does not ever need to be garbage collected, meaning less memory and performance overhead for using that object

Unlike most other convenience feature, this feature provides a clear performance benefit over our current closest equivalent.

Extensions

These are possible extensions to this proposal, feel free to suggest edits as I have not given deep thought to most of these.

Native functions' return values

This subheading does not directly concern the static syntax, but rather the internal workings of native functions, that would likely be implemented in C++

Currently, Generator#next() returns a new object like {value: any, done: boolean}. This object is dynamically allocated and different every time. This object could be statically allocated with its values mutated every time, potentially increasing performance substantially (although I expect the difference would be less significant the more sophisticated the JS runtime is)
Similarly, Map entry iterators return an array on each iteration. This array could also be statically allocated.

There are many other functions, native or not, that typically return dynamically allocated objects, such as TextEncoder#encode() or React's useState

Static variable declaration

Best described by an example, this would work similarly to classes' static, and would be specific to each function, essentially inheriting its function's lifetime.

function count(){
    static current = 0
    current++
    return current
}
console.assert(count() == 1)
console.assert(count() == 2)
console.assert(count() == 3)
console.assert(count.current == 3)

Let me know what you think!

1 Like

Related: C-style static variables

Yeah, that idea seems pretty much identical to the second extension I defined. Not quite the same as my main proposal though.

The difference becomes clear when you imagine a function B declared inside a function A

static variables are attached to the function they are defined in, very comparable to classes' static variables. If a static variable is declared in function B, it is initialised when B is initialised (i.e every time A is called)

static expressions aren't attached to anything, they are program-wide. In our same scenario, it's still only evaluated once, ever. Calling A won't reinitialise it.

These 2, while fairly comparable, fundamentally solve different problems.

Additionally, that post has a comment suggesting a way we can achieve the same result currently, which I describe as (one of) the things this proposal aims to avoid.

At what point does a static expression inside a function execute? The first time it's needed, or when the module first loads, or some other time?

This matters, because it effects what data the static expression is able to see. If they're being executed when the module first loads, then, well, they wouldn't be able to see any outside variables because no variables have been declared yet. If they execute the first time they're needed, then you risk the captured values becoming stale (which could be good or bad), e.g. if they use the function arguments to build an object, that object will forever contain the arguments that were there the first time the function ran.

1 Like

I had given some thought to this, and decided they execute at the beginning of the current module. This does mean that they don't have access to variables defined in that module, but static expressions aren't really meant for indeterministic code (while still possible), but rather simple expressions like an object literal or a preallocated buffer. Accessing any other variables in that module or function scope would result in a ReferenceError ('cannot use this variable in this way etc...')

Another option I have considered is that they evaluate at the top-most-parent-function's initialization, but decided against it due to how CommonJS modules run on node, which may cause issues.

Let me know what you think. Are there any better time to execute static expressions? Did I miss something in my current approach?

1 Like

They both have their trade-offs, so I don't know which I would prefer.

I do like the overall idea here. It would be nice to have a way to keep state specific to a function within that function.

I'm not sure how much traction a feature request like this would make though, since it's fairly trivial to work around the missing feature, but I still like it.