Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should once functions be marked as such, so libraries can throw them away? #6

Open
keithamus opened this issue Mar 29, 2022 · 13 comments
Labels
question Further information is requested

Comments

@keithamus
Copy link
Member

One common technique for using once is with the Event pattern, which allows for subscription to an event as a one-time acknowledgement, as opposed to seeing every event. Node.js has EventEmitter.prototype.once, and the DOM Standard has once as an option to addEventListener.

With regard to the event listener pattern, exposing to the listeners that this function is only desired to run once allows these implementations to cleanup after the initial event. For example NodeJS calls removeListener before dispatching to the function, ensuring the given function won't be called subsequent times, but also freeing up memory for weakly held references to the function:

function onceWrapper() {
  if (!this.fired) {
    this.target.removeListener(this.type, this.wrapFn);
    this.fired = true;
    if (arguments.length === 0)
      return this.listener.call(this.target);
    return this.listener.apply(this.target, arguments);
  }
}

If Function.prototype.once is exposed to developers, I imagine a preference may emerge that rather than using .once(foo), instead .on(foo.once()) is used. In this case, without a marker to determine this function is a "once function", implementation like Node's will not be able to remove the event listener.

I believe implementations should be able to tell a "once function" apart from a regular function.

Some possible ideas:

  • Function.isOnce(fn: Function): boolean
  • Function.prototype[Symbol.isOnce]: boolean
@js-choi js-choi added the question Further information is requested label Mar 29, 2022
@zloirock
Copy link

zloirock commented Mar 29, 2022

Functions produced by .bind, arrows, classes, async functions, generator function, etc. - why they haven't a such (at least simple) way, but functions produced by .once should have? I think that it should be resolved in the scope another proposal like https://github.com/caitp/TC39-Proposals/blob/trunk/tc39-reflect-isconstructor-iscallable.md / caitp/TC39-Proposals#3

@js-choi
Copy link
Collaborator

js-choi commented Mar 29, 2022

I suppose one may view once functions as state machines with two states—uncalled and already-called. In that sense, once functions are different from bound functions, async functions, etc. It may be reasonable to mark them and not the other sorts of function for that reason.

@zloirock
Copy link

Generators also are state machines - but we have no way to find is this .done or not without calling .next.

@js-choi
Copy link
Collaborator

js-choi commented Mar 29, 2022

Although that is true, there is no way to generally determine the state of a once function without an extra property. With generators, you can at least use check generator.next().done.

There is some precedent in some userland libraries, too; for example, onetime has a callCount method.

I’m neutral about this myself, personally, but it’s worth at least considering.

@zloirock
Copy link

zloirock commented Mar 29, 2022

you can at least use check generator.next().done

That mean calling of the the generator if it's not .done - no big difference with calling of function.

Another precedent is the state of promises.


I'm not against of this - I'm just for following the same design in the language everywhere without changing this design each new proposal (for the same reason I'm for the prototype method). If it will be added here - it should be added to other parts of the language, but this is wider than the scope of the current proposal.

@js-choi
Copy link
Collaborator

js-choi commented Jul 10, 2022

you can at least use check generator.next().done

That mean calling of the the generator if it's not .done - no big difference with calling of function.

Note that, although it is true that generator.next().done requires calling generator.next(), which may cause a side effect, at least its result’s done property definitively indicates the state of the generator.

In contrast, when you call a function ƒ(), no matter how many times you call it, there is no generic way to tell whether the ƒ() is doing anything, because you don’t know what ƒ is supposed to be doing. When you call ƒ(), it might or might not be printing something to the console; it might or might not be changing a variable or property somewhere; it might or might not be sending a network request—there’s no way for a Node EventEmitter or DOM EventTarget to tell whether anything is happening when it calls an event handler.

In order to tell whether ƒ() is doing anything, you have some a preexisting contract with the creator of ƒ—some standard variable or property of which you and ƒ’s author are both aware.


…Having said that, it may be the case that we don’t even want to design this once function for “once” Node EventEmitters or DOM EventTargets anyway. We might end up designing this proposal specifically for nullary callbacks. See #5 (comment) and #2 (comment).

@js-choi
Copy link
Collaborator

js-choi commented Jul 10, 2022

@keithamus: If we do end up supporting argument passing to callbacks (maybe using the approach in #5 (comment)), then I think that it would be good to allow any function—not just functions created by Function.once to expose to others whether it has become a side-effect-free “no-op”.

Maybe something like this:

const ƒOnce = ƒ.once();
ƒOnce.noop; // This is false.
fOnce(5);
ƒOnce.true; // This is now true.

An event system could check a callback’s noop property after every time it activates the callback. If the noop property has become true, then it the event system would remove the callback for garbage collection. Any function that sets its noop property would participate in this contract. Here’s a naïve example:

class EventEmitter {
  // This is a Set of event handlers.
  #handlerSet;
  
  addHandler (callback) {
    const handler = event => {
      callback(event);
      // Note that the callback is expected to set its noop property before it
      // returns, even if the callback is asynchronous.
      if (callback.noop) {
        this.#handlerSet.remove(handler);
      }
    });
    
    this.#handlerSet.add(handler);
  }
  
  emit (event) {
    for (const handler of this.#handlerSet) {
      handler(event);
    }
  }
}

@ljharb
Copy link
Member

ljharb commented Jul 10, 2022

imo this is a nonstarter. In the same way as we don't have an "is this an async function" idiom in the language - because async function is not the only way you can have a promise-returning function - we shouldn't have an "is this a one-use function", because Function.once isn't the only way you can have a one-use function.

@js-choi
Copy link
Collaborator

js-choi commented Jul 10, 2022

@ljharb: The idea of my snippet above was that any function—not just functions created by once—could participate in the system by marking itself as a “I am no-op from now on” with a .noop boolean property. That way, functions created by once would not be special; any function can mark itself.

For example, the function below does not use once at all:

let n = 0;
function printUpToThreeTimes () {
  if (n < 3) {
    console.log(n);
    n ++;
  } else {
    printUpToThreeTimes.noop = true;
  }
}

Adding this to an event system would allow the event system to know when the function will no longer do anything and remove the function from itself for garbage collection.

eventSystem.addHandler(printUpToThreeTimes);
eventSystem.emit(); // Prints 0.
eventSystem.emit(); // Prints 1.
eventSystem.emit(); // Prints 2.
eventSystem.emit(); // Does nothing *and* `printUpToThreeTimes` is removed from `eventSystem`’s handlers.

@ljharb
Copy link
Member

ljharb commented Jul 10, 2022

Sure, but then a function could lie - a multi-use function could claim to be "once", and a one-use function could fail to mark itself as "once".

@js-choi
Copy link
Collaborator

js-choi commented Jul 10, 2022

Indeed, the function can lie; the contract would be an honor system between the creators of the function and the event system.

If once sets the noop property, then community libraries might organically start to check noop, and other libraries might start to set noop, but honoring the contract correctly is the libraries’ responsibility, not once’s. All once would do is set .noop to true.

The question here is whether we want functions created by once to implicitly encourage such a contract by taking the lead in setting a noop property itself.

@ljharb
Copy link
Member

ljharb commented Jul 10, 2022

What’s the point of standardizing an honor system?

@js-choi
Copy link
Collaborator

js-choi commented Jul 10, 2022

I suppose it’s not really an “honor system” either. That is to say, the meaning of the noop flag would be relative to the author of the function. Nothing would stop a developer from writing this:

let n = 0, arr = [];
function addToArrUpToThreeTimes () {
  // This side effect occurs even if .noop is true,
  // because the function’s author consider it to be insignificant
  // compared to adding values to arr.
  console.log(n);
  if (n < 3) {
    n ++;
  } else {
    addToArrUpToThreeTimes.noop = true;
  }
}
addToArrUpToThreeTimes(0); // Prints 0; arr is now [0]; noop flag is false.
addToArrUpToThreeTimes(1); // Prints 1; arr is now [0, 1]; noop flag is false.
addToArrUpToThreeTimes(2); // Prints 2; arr is now [0, 1, 2]; noop flag is true.
addToArrUpToThreeTimes(3); // Prints 3; arr is still [0, 1, 2]; noop flag is true.

The meaning of the “noop” flag would be in the eye of the function’s author. A function can choose to set the flag on itself at any time, whether or not subsequent calls will cause literally no side effects. All the flag actually means is that the author considers any side effects by further function calls to be insignificant, and that the function is ready to be removed by any handler.

But although I do find this to be an interesting idea, I don’t feel super strongly about this.

Developers can write .once(Function.once(fn)) instead of .on(Function.once(fn)), and it probably will be fine, if not clunkier.

And we could always add this property in the future in a follow-on proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants