"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.

If this static feature came out, then React would be able to implement Solid.js signals and effects to replace Hooks without changing its rendering execution model and catch up to Solid speed. :smiley: :smile: :grin:

function MyReactComponent() {
  const [value, setValue] = static useSignal({ initial: 'value' })

  return ...
}

Maybe some global variable like globalThis.isStaticExpression() would be able to tell us if we're in a static expression (synchronous only), then React could just keep useState and the internal logic would know what to do:

function MyReactComponent() {
  const [value, setValue] = static useHook({ initial: 'value' })

  return ...
}

If you're placing the static keyword at the location where the function returns a triple, doesn't that mean the tuple would be pre-calculated, and you'd always get the same tuple every time the function runs? Which means you'd always get the same value for the state?

I'm also not sure a static keyword would help React - React component functions can create multiple component instances, each of which need their own copy of state. A static keyword implies we would be sharing something among all component instances, which doesn't fit into what the hooks (or signals) do.

1 Like

Returning a static triple doesn't make much sense (if one of the values is a plain object, you would place the static keyword there)
Note that statics still have access to global and module variables, so they can be used as an alternative to initialising the tuple outside of the function and returning the same every time. If per-function is what you need and not per-module, then static variables is what you want, e.g

const TopFunction = (x, y) => ({
  getSumAndProduct(){
    // Initialized at TopFunction()
    static pair = [x+y, x*y]
    return pair
  }
})

Ah, I underthought that a bit! :man_facepalming: Yeah, static won't help React because there's no way to make it static per "instance" of a component, the expression could only be static for all calls ever.

EDIT:

What if "cloning" a function would allow static expressions per per clone? For example something like the following (not sure "clone" is correct terminology):

function foo() {
  const thing = static {}
  return thing 
}

const foo1 = foo.clone()
const foo2 = foo.clone()

console.log(foo1() === foo1()) // true
console.log(foo2() === foo2()) // true
console.log(foo1() === foo2()) // false

Then, React could clone function components (one time per component "instance") to make the static expressions actually useful.

function MyComponent() {
  const [foo, setFoo] = static createSignal(123) // now this could work
}

the functionality you are describing is done with static variable declaration

function MyComponent(){
  static [foo, setFoo] = createSignal(123)
}

As per the proposal I described, foo and setFoo would be specific to MyComponent. If MyComponent was defined inside another function, or as a class field, then it would be specific to each instance of that parent function invocation/class instance

What if you have multiple instances of MyComponent? Won't they share the static variable?

Regarding the OP "static expr", it's essentially unnamed global variables sprinkled throughout the code, and just like global variables, initialized even if never used. Where's the performance benefit? All I see is an attempt to dodge one of the most difficult problems in software engineering: naming.

I can't see a good use case here. Memoization is a pretty bad one for static expr or static variables. You're basically creating a distributed cache that's difficult to measure, update or prune. And no you're not making the symbol completely hidden. It's still accessible via Object.getOwnPropertySymbols.

These would better be served with records and tuples.

For the example above, multiple instances of MyComponent would have separate static variables, unlike multiple invocations which would share the same value.

Static expressions or variable declarations are not meant to replace global variables. They are meant to fill the gap where global variables make sense from a performance perspective but not from a readability perspective.

Static expressions are not meant to be used to implement memoization but to make it effective. Memoization is almost completely useless when given two similar objects with different identity (i.e {} != {}). One solution is the records proposal, however they do not perform nearly as well as static expressions would and would require changes to existing libraries whereas this would not. (Additionally consider that we would see potential improvements in more cases than just memoization)

It could be initialized on first use.

How could they have separate static variables, when there's still only one MyComponent function?

The thing is, react calls the function multiple times per component instance (as many times as it needs to re-render), which is what @theScottyJam was reminding us.

If you have this component:

function MyComponent ({count}) {
  ... static expression, or static variable, doesn't matter ...

  console.log("MyComponent", count)

  return ...template...
}

And then you use it like this:

function OtherComponent() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    setInterval(() => setCount(count + 1), 1000)
  }, [])
  
  return <>
    <MyComponent count={count} />
    <MyComponent count={count} />
  </>
}

Then what will happen is every second the MyComponent function will be executed twice.

After 10 seconds MyComponent will have been called 22 times, logging "MyComponent" 22 times, but there are still only two "instances".

All 22 of those calls would share the single static expression or static variable.

OtherComponent will have been called 11 times if it was used in a single other JSX location.

There's no way to determine, purely from the function being called, how many "instances" React is tracking.

That's why I thought maybe that function "clone" idea would be interesting, to allow clones to create separate static context (on invocation, not ahead of time)

With the clone feature, React could clone a component per "instance".


On the other hand, Solid.js runs a component function a single time per instance, so static expr or variable would be useful for shared state across all components (function clone idea would be pointless there)

@lightmare yeah we can use globals, but the nice thing about static is they are not globally exposed; they are lexically scoped despite global behavior.

might be nice to also specify which scope:

function foo() {
  static module /*...*/; // effectively per source location 
  static outer /*...*/; // nearest scope around the function at execution
  static /*...*/; // defaults to one of the above
}

Compare:

function creatFunc() {
  return function foo() {
    static module /*...*/; // shared static no matter how many of these functions are created
  }
}

vs

function createFunc() {
  return function foo() {
    static outer /*...*/; // static per foo function based on each time it is created in scope of createFunc()
  }
}

where foo.clone() would always create a new function with a new static scope (in that case it doesn't matter which static scope the original had).

I don't see why module scopes, block scopes, or the occasional IIFE scope do not suffice for any of the mentioned use cases. A new static syntax doesn't bring that much benefit imo to warrant the introduction of new syntax.

Also, I really dislike global behavior that is hidden deep within code and not declared up front at the top level.

1 Like

Of course those devices work. This is perhaps more about ergonomics and getting more joy writing.

We didn't really need class syntax, right? But it made expression better.

Me too!

But this is different: the behavior is local to where it is defined, and does not affect outer scope in any way. You only read the static expression right at the location where it is defined, and it has no meaning outside of that location. The meaning is not shared with other code.

This,

function foo() {
  return thing ?? static Object.freeze([])
}

Is potentially nicer to read and easier to write than this:

export const defaultArray = Object.freeze([])
import {defaultArray} from './consts.js'

function foo() {
  return thing ?? static Object.freeze([])
}

They are different: With static, the static value is available only to that specific source location, while the regular car is available to any code anywhere.


Just imagine a system like React though! It would be very unergonomic to hoist out all the expressions from all components that have them.

The difference is that classes are very common in JS code. I argue that static variables are not.

Why did you put the object in a separate module for no reason? I don't see much of a difference between

export function foo() {
  return thing ?? static Object.freeze([]);
}

and

const defaultArray = Object.freeze([]);
export function foo() {
  return thing ?? defaultArray;
}
1 Like