tc39 / proposal-function-once

A TC39 proposal for an Function.prototype.once method in the JavaScript language.
BSD 3-Clause "New" or "Revised" License
46 stars 3 forks source link

Function.prototype.once vs. Function.once vs. @Function.once #1

Open js-choi opened 2 years ago

js-choi commented 2 years ago

Should we go with an “instance” method on the prototype or a “static” function on the constructor?

Function.prototype.once

This has precedent from, e.g., Function.prototype.bind.

Examples From [execa@6.1.0][]: ```js export function execa (file, args, options) { /* … */ const handlePromise = async () => { /* … */ }; const handlePromiseOnce = handlePromise.once(); /* … */ return mergePromise(spawned, handlePromiseOnce); }); ``` From [glob@7.2.1][]: ```js function Glob (pattern, options, cb) { /* … */ if (typeof cb === 'function') { cb = cb.once(); this.on('error', cb); this.on('end', function (matches) { cb(null, matches); }) } /* … */ }); ``` From [Meteor@2.6.1][]: ```js // “Are we running Meteor from a git checkout?” export const inCheckout = (function () { try { /* … */ } catch (e) { console.log(e); } return false; }).once(); ``` From [cypress@9.5.2][]: ```js cy.on('command:retry', (() => { /* … */ }).once()); ``` From [jitsi-meet 1.0.5913][]: ```js this._hangup = (() => { sendAnalytics(createToolbarEvent('hangup')); /* … */ }).once(); ```

Function.once

It might be easier to read inline function expressions with a prefixed constructor function? It’s more verbose, though, when including the Function constructor as a “namespace”.

Examples From [execa@6.1.0][]: ```js export function execa (file, args, options) { /* … */ const handlePromise = async () => { /* … */ }; const handlePromiseOnce = Function.once(handlePromise); /* … */ return mergePromise(spawned, handlePromiseOnce); }); ``` From [glob@7.2.1][]: ```js function Glob (pattern, options, cb) { /* … */ if (typeof cb === 'function') { cb = Function.once(cb); this.on('error', cb); this.on('end', function (matches) { cb(null, matches); }) } /* … */ }); ``` From [Meteor@2.6.1][]: ```js // “Are we running Meteor from a git checkout?” export const inCheckout = Function.once(function () { try { /* … */ } catch (e) { console.log(e); } return false; }); ``` From [cypress@9.5.2][]: ```js cy.on('command:retry', Function.once(() => { /* … */ })); ``` From [jitsi-meet 1.0.5913][]: ```js this._hangup = Function.once(() => { sendAnalytics(createToolbarEvent('hangup')); /* … */ }); ```

Decorator

@Function.once
function f () { /* … */ }

Multiple

Having two or three of the above is an option. They would probably have to have different names, because the Function constructor is itself a function.

VitorLuizC commented 2 years ago

Why don't we have both?

js-choi commented 2 years ago

Yes, adding both is also an option, though it makes me ask if then it would not be appropriate to do the same for bind: adding a Function.bind in addition to Function.prototype.bind.

zloirock commented 2 years ago

Adding static Function.bind will be a breaking change. I saw (and used) Function.bind as a shortcut for Function.prototype.bind too many times, like

const bind = Function.call.bind(Function.bind);
zloirock commented 2 years ago

I'm for Function.prototype.once for consistency with .bind. In case of adding static equals, they should have different names.

js-choi commented 2 years ago

Adding static Function.bind will be a breaking change. I saw (and used) Function.bind as a shortcut for Function.prototype.bind too many times…

Ah, yeah, silly me. I had forgotten that the Function constructor itself is a function…

hax commented 2 years ago

Yeah, because Function is also a function, I generally dislike any new instance method on Function.prototype.

For once, there is also a small readability problem of

let f = function a_big_func() {
  /* many 
      many
      code */
}.once()  // <- only see once here

Note, though Function.once + (pipeline op / extension methods) also could have similar issue, they are developer choices to write code like that, on the other side, Function.prototype.once force developer write code like that by default.

hax commented 2 years ago

I'm for Function.prototype.once for consistency with .bind.

I don't think F.p.bind create any strong consistency requirements, F.p.bind have too many design flaws. 🤪

theScottyJam commented 2 years ago

It's not uncommon for developers to want callable objects. To do that, you create a function, and then attach arbitrary properties to it, like this:

const myFn = Object.assign(function() {...}, { ... })

Adding new functions to the Function.prototype has a higher likelihood of causing breaking changes, because of how common the above pattern is.


Let me also add a third option. This could also be done as a decorator, like this:

@Function.once
function() { ... }
hax commented 2 years ago

Yeah, consider decorator advanced to stage 3, I hope we could revisit function decorators in future meetings.

zloirock commented 2 years ago

I'm strictly for a prototype method since it's simpler for usage in most cases.

lygstate commented 2 years ago

Vote for Function.once only, like Object.hasOwn,

ljharb commented 2 years ago

I think a prototype method isn’t sufficiently useful, and it belongs only as a static method.

Shipping a decorator wouldn’t make sense because you can’t decorate standalone functions nor object property values.

theScottyJam commented 2 years ago

Shipping a decorator wouldn’t make sense because you can’t decorate standalone functions nor object property values.

Yet... aren't there plans for a follow-on proposal for that? I hope so, because it would be odd to restrict a feature as useful as decorators so they only work within classes. Otherwise, the same argument could be made about anyone trying to make almost any kind of decorator - "You can't use decorators outside of classes, so don't use a decorator".

But, I can understand not wanting to block this proposal on another proposal that hasn't even been presented, nor do we know if it would ever go through. It would just be a bit of a shame, as this seems like the exact kind of thing that decorators were built to do.

js-choi commented 2 years ago

Function.prototype.once would keep open the possibility of a @Function.once decorator, for whenever function decorators hopefully get standardized in the future.

In contrast, a Function.once static method would probably permanently exclude a @Function.once decorator, even after function decorators get standardized.

(The same is true for Function.prototype.memo, @Function.memo, and Function.memo; see js-choi/proposal-function-memo#2.)

ljharb commented 2 years ago

@js-choi i'm not sure why; a function can know when it's being called as a decorator, so Function.once could take a function, or also be called as a decorator. That said, I don't think it would really ever make sense to be a decorator; its use case is usually for callbacks, not for instance methods.

hax commented 2 years ago

@ljharb Detecting whether being called as a decorator rely on the structural type of the parameter, which is not accurate. Some mechanism like new.target may be better.

And there is also another way, we could add a custom hook via well-known symbol like Symbol.decorator, so @foo could always use foo[Symbol.decorator], and Function.prototype[Symbol.decorator] could be get [Symbol.decorator]() { return this }.

js-choi commented 2 years ago

a function can know when it's being called as a decorator, so Function.once could take a function, or also be called as a decorator.

That’s true. We could distinguish decorator uses (@Function.once …) from ordinary function calls (Function.once(…)).

So if we used a Function.once static function, it may be future-compatible with extending it to be a function decorator. This still leaves unresolved the question on whether including Function.once means we should not have the instance method Function.prototype.once.

That said, I don't think it would really ever make sense to be a decorator; its use case is usually for callbacks, not for instance methods.

I’m not thinking about decorating instance methods but rather looking towards future general function decorators. It may make sense for the author of a side-effect function to ensure that it will only be executed at most once: @Function.once function executeEffect () {}.

Detecting whether being called as a decorator rely on the structural type of the parameter, which is not accurate. Some mechanism like new.target may be better.

And there is also another way, we could add a custom hook via well-known symbol like Symbol.decorator, so @foo could always use foo[Symbol.decorator], and Function.prototype[Symbol.decorator] could be get [Symbol.decorator]() { return this }.

Although this is a creative idea, do you have specific examples of problems that would occur from type-based polymorphism of Function.once’s argument? (I would rather not this proposal depend on yet another well-known-symbol-based metaprogramming system.)

ljharb commented 2 years ago

@js-choi function executeEffect() {} |> Function.once(^), also - the only benefit of a decorator there is on a function declaration.

js-choi commented 2 years ago

function executeEffect() {} |> Function.once(^) would not declare executeEffect in the current environment—rather, it is an expression of a function named executeEffect that gets wrapped in once before disappearing. Is that what you meant?

That is, people who wish to declare functions that are used at most once would have to do things like this:

const executeEffect = function executeEffect() {}.once();

…or:

const executeEffect = function executeEffect() {} |> Function.once(^^);

…rather than:

@Function.once function executeEffect() {}

This use case might not be a big deal, but I think it’s worth at least considering.

ljharb commented 2 years ago

Yes, that's what i meant - they'd do const executeEffect = Function.once(function executeEffect());, or via pipeline.

js-choi commented 2 years ago

Alright, thanks. Given that we could use type polymorphism for any future decorator form, let’s ignore decorators for now.

We’ve basically got three choices: ƒ.once, Function.once(ƒ), and both.

ƒ.once() is more consistent with ƒ.bind(receiver) than Function.once(ƒ).

However, Function.once’s prefix form may make it easier to read when using it with an inline function block, compared to .once()’s suffix form:

const ƒ = function () {
  /* very long body with many lines */
}.once();

…is less readable than:

const ƒ = Function.once(function () {
  /* very long body with many lines */
});

Relatedly, I’ve usually seen developers (including myself) use .bind with already-declared function variables or function properties:

functionVariable.bind(receiver)
object.property.chain.bind(receiver)

…rather than using .bind on inline function blocks:

function () {
  /* very long body with many lines */
}.bind(receiver);

In this way, one could argue that the situation between .bind and once is quite different, and that .bind should not be used as a strong precedent on the form of once.

(We could also have both, yes, though I don’t know of any precedent in the language for having both, and .once() could always be replaced by |> Function.once(^^).)

lygstate commented 2 years ago

Alright, thanks. Given that we could use type polymorphism for any future decorator form, let’s ignore decorators for now.

We’ve basically got two choices:

ƒ.once() is more consistent with ƒ.bind(receiver) than Function.once(ƒ).

However, Function.once’s prefix form may make it easier to read when using it with an inline function block, compared to .once()’s suffix form:

const ƒ = function () {
  /* very long body with many lines */
}.once();

…is less readable than:

const ƒ = Function.once(function () {
  /* very long body with many lines */
});

Relatedly, I’ve usually seen developers (including myself) use .bind with already-declared function variables or function properties:

functionVariable.bind(receiver)
object.property.chain.bind(receiver)

…rather than using .bind on inline function blocks:

function () {
  /* very long body with many lines */
}.bind(receiver);

In this way, one could argue that the situation between .bind and once is quite different, and that .bind should not be used as a strong precedent on the form of once.

How about both Function.once Function.bind and Function.prototype.once and Function.prototype.bind exist?

For example Object.hasOwn also do the thing alike

js-choi commented 2 years ago

Can you elaborate by what you mean by “Object.hasOwn also do the thing alike”? We put functions like hasOwn on the Object constructor, rather than Object.prototype, because changing Object.prototype would affect nearly all objects in all codebases, including plain JavaScript objects and third-party classes that do not subclass null.

We could have both Function.once and Function.prototype.once, yes, though I don’t know of any precedent in the language for having both a static function and an instance method with the same functionality. And .once() could always be replaced by |> Function.once(^^) (see the pipe-operator proposal. The Committee might balk if we try to add both Function.once and Function.prototype.once.

ljharb commented 2 years ago

We definitely don't need both; if we could get rid of Object.prototype.hasOwnProperty, we would, and it's not the same case as this because objects sometimes have null prototypes, whereas functions virtually never do.

(altho, that you can Object.setPrototypeOf(f, null) may be another argument for a static over an instance method)

littledan commented 1 year ago

My subjective take: I like Function.once better than .once. It feels like it gives you something to "grab onto" semantically, and it's ugly to require the parens around the function or arrow literal to use the method on it.