Static call-site

New language feature... as a prelude In template literals the array passed as an argument to tag is static and only ever created once, such as in the following example

var arr = [];
var tag = (a) => { return a; };
var exp = (a) => { arr.push(tag`abc${a}`); };
exp(1);
exp(2);
console.assert(arr[0] === arr[1]);

What of the idea to be able to designate a call site to be static in so much as it allows us to mimic tagged template literals callsite arguments, for example:

function useState(static ...args) {
// here i can be sure that arguments is always the same for each call site.
}

while (true) {
   useState(1)  // different from the next, but same on each iteration
   useState(2)
}

Where by the call to useState(1) is always called with a shared arguments object in relation to it's callsite, as is useState(2), but they are in relation to each other different.

Alternatively it could be.

static function useState(a, b) {
   return Math.random(1) // this is only every called once per call site.
}

If it is to be, It is important that this exist as a syntax for the provider to specify rather than for the consumer such that consumers of the function would always still just call the function without caring for this semantics.

I would disagree here. Normally, if I have logic like this:

function doThing() {
  f()
  g()
}

I know I can safely extract g() into a new function, without affecting the way the program runs in any way.

function doThing() {
  f()
  helper()
}

function helper() {
  g()
}

With a new feature like this, we're introducing a new, "magical" feature that can make harmless refactorings into dangerous ones. I wouldn't want a feature like this, unless the caller is forced to use some piece of syntax at the call site, to indicate that there's some extra magic going on.

function doThing() {
  withState f()
  g()
}

A couple of other issues that exist, unless we force end-users to call these sorts of functions with different syntax.

  1. What happens if you're not the one calling it, but the built-in libraries are doing the calling?
const useState = (static x) => x
array.map(useState)
array.map(useState)

Do both occurances of array.map(useState) count as being called from the same location? Because in both cases its the JavaScript engine that's calling it? There's plenty of times when the engine calls a function for us (e.g. the iterator protocol, other callback-based APIs, the toString protocol, etc).

  1. This could make libraries more fragile, and more difficult to refactor.

Say, you have a library like this:

export function doThing(callback) {
  if (condition) {
    callback(x)
  } else {
    callback(y)
  }
}

Today, you're allowed to refactor this like so, without it being a breaking change:

export function doThing(callback) {
  callback(condition ? x : y)
}

But, as this proposal currently stands, this type of refactor would technically be a breaking change, because a user of this library could be supplying a callback with one of these static parameters, making it so the library user would notice if you changed from calling it in two locations to one location.

I probably shouldn't say this, but this is theoretically possible, if you're willing to create an error object, examine its stack trace, see where the caller is located, then cache data depending on who called you.


An important part of any proposal is to present some use cases, showing why such a feature would be invaluable. Do you think you could explain why you feel this feature would be a helpful addition to the language?

1 Like

Why would it not run the same way here as well? the call site is still unique whether in helper or otherwise. You cannot have two callsites at the same location so this should not be an issue... a better demonstration is you next example:

Say, you have a library like this: ...

But this is exactly what you want to happen for this case, in fact that is the core functionality this would allow, that you cannot currently do. To link the execution to a callsite, such that one can delimit that each unique callsite to this function should only ever execute once.

What happens if you're not the one calling it, but the built-in libraries are doing the calling?

Those will all be one callsite: the site that they are actually called, not where they are passed as arguments. So in that case they would both share a similar call site similar to how the following would as well:

static function useState(a, b) {
   return Math.random(1) // this is only every called once per call site.
}

function singleCallSite(...args) {
    return useState(...args) // same callsite for all calls
}

singleCallSite(1)
singleCallSite(1)

Example use cases: Implementing React hook-like apis in the same vein as useState etc.

The call stack of react hooks is AFAIK different over repeated calls, even for a given instance of a component located at a certain point of the components tree, it will have the full App tree on first render, but a much shorter stack when updating just that component.

I think what you really want here I think is algebraic effects handlers (a generalization of the exception mechanism to arbitrary effects) and possibly session types (a way to define what effects are legal in a given situation, in what order, i.e. a way to define protocols for effects).

Whoops, you're right. The other example was a better demonstration of this refactoring hazard.

If the library intends the user to supply these types of special callbacks, then sure, this is expected behavior, and the library author would have to be aware of this sort of refactoring hazard. But, the point is we're now turning something that's currently private knowledge into public knowledge, and by so doing we're restricing how libraries can refactor their internal details. This is why I think it might be better as an opt-in feature, so if a library doesn't use special call syntax to enable this sort of feature, then the user can't use it, and the library is more free to refactor however they need.

But, either way, I'm ok if we disagree on this point.

This can't actually be used to implement React hooks.

  1. You can create multiple instances of the same component, each with different state. This meens a single useState() call is in charge of multiple pieces of state, one piece of state for each component instance. This proposal would only allow it to provide a single, global piece of state.
  2. I can create a custom useState hook like this:
function myUseState(...args) {
  return React.useState(...args)
}

This custom hook behaves exactly the same as React's use state. I can't do this sort of thing with this proposal, because each time you call myUseState(), you're going to be using the same React.useState() function, no matter where you called myUseState() from.


On another note, I've thought of a couple of ways to replicate this sort of proposal in userland today (better then inspecting a stacktrace).

  1. You could actually use the template tag trick you were showcasing. It's certainly hacky, but it works.
const internal = Symbol()
const staticKey = array => ({ [internal]: array })

// Helper functions, or decorators
// could help remove some of this
// WeakMap boilerplate.
const allState = new WeakMap()
function useState(key, initialState) {
  if (!key[internal]) throw new Error('Bad key provided')
  const setState = newValue => { allState.set(key[internal], newValue) }
  if (allState.has(key[internal])) {
    return [allState.get(key[internal]), setState]
  }
  allState.set(key[internal], initialState)
  return [initialState, setState]
}

// ---------- //

function getCounter() {
  const [count, setCount] = useState(staticKey``, 0)
  const [resetCount, setResetCount] = useState(staticKey``, 0)
  return {
    increment() {
      setCount(count + 1)
    },
    get count() { return count },
    reset() {
      setCount(0)
      setResetCount(resetCount + 1)
    },
    get resets() { return resetCount },
  }
}

const counter1 = getCounter()
console.log(counter1.count, counter1.resets) // 0 0
counter1.increment()
const counter2 = getCounter()
console.log(counter2.count, counter2.resets) // 1 0
counter2.reset()
const counter3 = getCounter()
console.log(counter3.count, counter3.resets) // 0 1

The trick is that the specialKey template tag will always return the array its handed (albiet, wrapped in an object in this example). Since the array is unique to the place where the template tag was used, we can use it as a WeakMap key, and assosiate state with this particular Array instance. You can play around with the example code yourself to see how it works - you'll notice its behavior veries in many ways from how real hooks work, partly due to the issues I described above, and also due to simple facts like there's no concept of a "re-render" outside of a component.

There's a simpler solution though. It's a bit more verbose, but it's also more standard and easier to understand. This solution is to simply create an instance at the module-level that holds the state you need, then call methods from it inside your specific functions. Here's the example same example, but written so that each useState() call has an assosiated instance on the module-level.

class State {
  #state = null
  #initialized = false
  useState(initialState) {
    if (!this.#initialized) {
      this.#initialized = true
      this.#state = initialState
    }
    return [this.#state, value => { this.#state = value }]
  }
}

///////////

const countState = new State()
const resetCountState = new State()

function getCounter() {
  const [count, setCount] = countState.useState(0)
  const [resetCount, setResetCount] = resetCountState.useState(0)
  return {
    increment() {
      setCount(count + 1)
    },
    get count() { return count },
    reset() {
      setCount(0)
      setResetCount(resetCount + 1)
    },
    get resets() { return resetCount },
  }
}

const counter1 = getCounter()
console.log(counter1.count, counter1.resets) // 0 0
counter1.increment()
const counter2 = getCounter()
console.log(counter2.count, counter2.resets) // 1 0
counter2.reset()
const counter3 = getCounter()
console.log(counter3.count, counter3.resets) // 0 1
1 Like

I don't get that example:

What would be the argument in

function useState(static ...args) {
  // here i can be sure that arguments is always the same for each call site.
}

for(let i=0; true; i++) {
   useState(i)  // different from the next, but same on each iteration
   useState(-i)
}

Would it always pass 0 to the first and -0 to the second? Would it prevent further calls at the same site with different values? Nothing I can think of really makes sense.

I'm with @theScottyJam on this, you should use explicit partial application in the required scope if you want multiple calls to share information.