Lazy inlined constants

Proposal is to allow creating lazy inlined global constants.

It will allow to make more performant, compact and less fragmentized code, and will help to avoid some common issues like in Example 2.

Example 1

// Bad: array is created on each call
const doSomething = () => {
  if (["a", "b", "c"].includes(x)) {
    ...  
  }
}

// Good: array is created lazily once and stored as global constant
const doSomething = () => {
  if (@["a", "b", "c"].includes(x)) {
    ...  
  }
}

Example 2

const selector = (state) => {
  // Bad: selector is not pure, it returns different empty array each time,
  // which causes re-render on each state change
  if (!state.items) {
    return []  
  }
  ...

  // Good: always returns the same empty array,
  // created lazily as global constant
  if (!state.items) {
    return @[]
  }
}

Example 3

// Bad: styles are created on app launch, which slows the app launch
const styles = StyleSheet.create({
  container: {
    ...
  }
})

// Good: styles are not created here yet
lazy const styles = StyleSheet.create({
  container: {
    ...
  }
})

const Component = () => {
  // they are lazily created when accessed for the first time
  return <App style={styles.container}/>
}

// another version
@const styles =...

Example 4

// Bad: default options are created on each function call,
// or it requires creating constant explicitly outside, which requires
// more time and over-fragmentize the function code.
const doSomething = (options) => {
  if (!options) {
    options = {
      isDev: true,
    }
  }
}

// Good: default options are created lazily once and stored
// as a global constant, code is not fragmentized.
const doSomething = (options) => {
  if (!options) {
    options = @{
      isDev: true,
    }
  }
}

Example 5

// Bad: creating keyExtractor on each render call ruins
// memoization of MemoizedComponent (React)
const Component = (props) => {
  return (
    <MemoizedComponent
      keyExtractor={(item) => item.id}
      ...
    />
  )
}

// Good: keyExtractor is created lazily once as a global constant,
// without useCallback or code fragmentation
const Component = (props) => {
  return (
    <MemoizedComponent
      keyExtractor={@(item) => item.id}
      ...
    />
  )
}

Example 6

const doSomething = (array) => {
  // Bad: it is a good practise to move some code to a separate
  // scope without fragmentation to different functions,
  // but here function is created on each call.
  const values = (() => {
    const a = ...
    const b = ...
    const y = ...
    ...
   return array.filter(x => x !== y)
  })()

  // Good: function is created globally once, lazily,
  // and its previous closure is passed as usual argument
  const values = (@() => {
    const a = ...
    const b = ...
    const y = ...
    ...
   return array.filter(x => x !== y)
  })()
}

A question:

What is the behavior of this?

const f = a => @() => a

f(2)()
console.log(f(3)());

Without the "@", it would log out "3". Because the "@" is there and the function is created only once, does it then capture its first closure only? So this example will log out "2", not "3"?


Separately, I assume example 3 could be rewritten as follows, so that you can re-use the same syntax that the rest of your examples are using? Instead of needing a new syntax for this specific case?

const styles = () => @StyleSheet.create({
  container: {
    ...
  }
})

const Component = () => {
  return <App style={styles().container}/>
}

Is being explicit about this too verbose?

let cachedArr;
const doSomething = () => {
  if ((cachedArr ??= ["a", "b", "c"]).includes(x)) {
    ...  
  }
}

// **Example 2**


let cachedEmpty;
const selector = (state) => {
  if (!state.items) {
    return (cachedEmpty ??= []);
  }
  ...

Seems similar to: Proposal: Function static variables and blocks

1 Like
  1. I guess in that case error can be shown. Either that we don't allow inlined constants contain non global constant closures, or, if we implement example 6, something explaining that case, need to think about it.

  2. Actually just a lazy constant can probably go to a separate proposal, with lazy const syntax. In you example 1) styles function is not lazy 2) requires function call each time, for no good reason

The trend in non low level system development is that readability is more important than performance. And while your examples work good, if something doesn't look nice, short and fast-readable then you will hardly find it in most of the apps.

Also, using ??= will look even worse for the 3 example.

1 Like

Yeah, thanks, looks really similar, but this proposal is not "sweet" enough for a modern language IMO, it won't allow to use something like @[] that easy.

However I found do expressions proposal as a better alternative to example 6, so we can refuse to support non-global constant closures.

Could you expound on your example two use case? What would an example look like for someone calling this function? You're talking about rerenders, so I presume this has something to do with a framework like React?

This particular selector can be used for example as react-redux selector.

It will cause re-render on each redux state change when !state.items is true, because empty array is new every time and useSelector uses === to determine if its subscriber needs to be re-rendered.

Usage example:

const Component = () => {
  const data = useSelector(selector)
  ...
}

In C# by the way there is static T[] Array.Empty<T>() method which creates single instance of empty array for the entire app.

Ok, got it.

This, however, does seem like an anti-pattern way of using useSelector()? The purpose of useSelector seems to just be for returning a bit of state from the state tree. If you're wanting to process the state to make it nicer to consume, that seems like something one would do outside of the selector function, like this:

const selector = state => state.items

const Component = () => {
  const rawItems = useSelector(selector);
  const items = rawItems ?? [];
}

Of course, that could be wrapped in a hook of its own to make it easier to consume.

const selector = state => state.items

const useItems = () => {
  const rawItems = useSelector(selector);
  return rawItems ?? [];
};

const Component = () => {
  const items = useItems();
}

Alternatively, in many instances, you can just keep the state tree cleaner so post-processing isn't required.

And yet another option is to provide your own comparison function to useSelector().

While "@" does also work to handle this use-case as well, it seems to be encouraging behavior that shouldn't be done in the first place.

That being said - this is just what I'm gathering from reading the docs - I've never used Redux's newer APIs, so I don't have personal experience with it and its limitations, so feel free to call me out if I'm completely off the wall here, or maybe provide an alternative example where solutions like "simply keeping the state tree" wouldn't actually work.

  1. The main issue of example 2 can be described as "It is easy to break memoizations", because when you return new empty array each time - memoized code stops working.

In your example even If you do it in component, this array can break memoization of rendered component where you pass the data.

const Component  = () => {
  const data = useSelector(dataSelector) ?? []
  // memoization won't work
  return <MemoizedCompoent data={data} />
}
  1. There is also another issue - you create new empty array on each render, and the old one is garbage collected. It is a totally useless waste of resources. This problem can be seen in other examples, e.g. 1.

  2. And of course even if there is a better practise - like returning the part of state without providing default values, not many people follow it as I observe in my teams. And often people follow another kind of "best practise" (at least they think so) like trying to always provide default values to prevent "null reference" kind of errors. Especially in JS, where there is no compile time type checks.

1 Like
const doPop = () => {
  return @["a", "b", "c"].pop()
}

const doPush = () => {
  return @["a", "b", "c"].push("d")
}

doPop();  // "c"
doPop();  // "b"  ?  "c"?  or undefined?
doPush(); // should the lazy constants be the same ?  

I think it is pretty obvious that constant structures should be readonly, for example wrapped into Object.freeze(). So the error will be thrown here.

Best practices for React aside, I believe that the main point was that making a new empty array each time the default value is needed is wasteful no matter what pattern it happens to be.

So in react, even with the "best practice" you'd still want the feature:

const Component = () => {
  const rawItems = useSelector(selector);
  const items = rawItems ?? @[]; // <-- here, we still want to avoid unnecessary reallocation.
}

Are you sure that the ?? [] is actually allocating anything meaningful? Engines are pretty good at optimizations these days.

If it were, then const empty = []; outside the function seems pretty straightforward.

@simonkcleung

Those are two separate static expressions, and because a static expression runs once, this would be the result:

doPop();  // "c"
doPop();  // "c"
doPop();  // "c"

doPush(); // 4
doPush(); // 4
doPush(); // 4

The same as if you defined it like this:

const doPop = () => {
  return @( ["a", "b", "c"].pop() ) // expression inside parens runs once, evaluating to "c"
}

const doPush = () => {
  return @( ["a", "b", "c"].push("d") ) // expression inside parens runs once, evaluating to 4
}

It would be different if we did this:

const doPop = () => {
  return @( ["a", "b", "c"] ).pop() // static expression was the array only, the pop() always runs on each call
}

const doPush = () => {
  return @( ["a", "b", "c"] ).push("d") // static expression was the array only, the push() always runs on each call
}

Output:

doPop();  // "c"
doPop();  // "b"
doPop();  // "a"
doPop();  // undefined

doPush(); // 4
doPush(); // 5
doPush(); // 6

I would prefer the syntax not to change semantics of the declared objects, so I'd you wanted a constant array you would need to be explicit about that:

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

or with static syntax:

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

Yeah it is, that array can be mutated and it is a different reference each time.

Sure, all the discussed use cases can be achieved today. F.e.:

globalThis.defaultArray = []

function foo() {
  return thing ?? defaultArray
}

This is about nice sugar. Plus this new syntax has an advantage: the static expressions or variables are lexically scoped, whereas a const or global var is polluting the outer scope. The sugar keeps things more concise without affecting outer scope.

I really like the idea of lazy but I think it is a separate idea from static/@.

lazy would help optimize big libraries a lot. For example, imagine that you could prevent a class from being defined until the moment you use it for the first time:

Lib author:

// lib/index.js
export lazy @deco class Foo {....}
export lazy @deco class Bar {....}

Lib user:

import {Foo} from "lib/index.js" // but not Bar

setTimeout(() => {
  new Foo() // defines the class, then instantiates it.
}, 1000)

// Bar is not defined if never used

For example, any decorators that may be applied on Bar will never even run if Bar is not used. Foo was defined and Foo decorators were applied only right when the first use of Foo happened one second after user module evaluated.

Basically it would be kinda like runtime tree shaking, and semantics would differ slightly (f.e. relying on decorators synchronously sharing state in scope of a module would not be the same anymore if decorators run later, etc).

A huge library that a user only needs a few things from could avoid instantiating a bunch of unused stuff.

Babylon.js is an example library that gives you the banana, gorilla, and whole jungle when all you wanted was the banana (for example just look at the Scene class and follow all the imports from there to see all sorts of things that are newed). Startup time for Babylon could be improved, without major refactoring, if it simply introduced lazy in a bunch of places, to avoid instantiating anything for features a user does not use.

Introducing lazy regardless of the static feature, would open the door to a huge amount of optimization of existing libraries in an easy way.

Oh, hmmm! lazy could also eliminate the use of /*@__PURE__*/ comments with bundlers in a bunch of cases perhaps?