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.

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.

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 ?