proposal-function-once icon indicating copy to clipboard operation
proposal-function-once copied to clipboard

Function.prototype.once vs. Function.once vs. @Function.once

Open js-choi opened this issue 2 years ago • 25 comments

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 [email protected]:

export function execa (file, args, options) {
  /* … */
  const handlePromise = async () => { /* … */ };
  const handlePromiseOnce = handlePromise.once();
  /* … */
  return mergePromise(spawned, handlePromiseOnce);
});

From [email protected]:

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 [email protected]:

// “Are we running Meteor from a git checkout?”
export const inCheckout = (function () {
  try { /* … */ } catch (e) { console.log(e); }
  return false;
}).once();

From [email protected]:

cy.on('command:retry', (() => { /* … */ }).once());

From jitsi-meet 1.0.5913:

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 [email protected]:

export function execa (file, args, options) {
  /* … */
  const handlePromise = async () => { /* … */ };
  const handlePromiseOnce = Function.once(handlePromise);
  /* … */
  return mergePromise(spawned, handlePromiseOnce);
});

From [email protected]:

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 [email protected]:

// “Are we running Meteor from a git checkout?”
export const inCheckout = Function.once(function () {
  try { /* … */ } catch (e) { console.log(e); }
  return false;
});

From [email protected]:

cy.on('command:retry', Function.once(() => { /* … */ }));

From jitsi-meet 1.0.5913:

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.

js-choi avatar Mar 18 '22 02:03 js-choi

Why don't we have both?

VitorLuizC avatar Mar 18 '22 03:03 VitorLuizC

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.

js-choi avatar Mar 18 '22 04:03 js-choi

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 avatar Mar 18 '22 04:03 zloirock

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

zloirock avatar Mar 18 '22 04:03 zloirock

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…

js-choi avatar Mar 18 '22 04:03 js-choi

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 avatar Mar 19 '22 02:03 hax

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. 🤪

hax avatar Mar 19 '22 02:03 hax

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() { ... }

theScottyJam avatar Mar 21 '22 21:03 theScottyJam

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

hax avatar Mar 29 '22 13:03 hax

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

zloirock avatar Mar 29 '22 16:03 zloirock

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

lygstate avatar Apr 15 '22 09:04 lygstate

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.

ljharb avatar Apr 15 '22 16:04 ljharb

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.

theScottyJam avatar Apr 15 '22 16:04 theScottyJam

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.)

js-choi avatar Apr 17 '22 20:04 js-choi

@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.

ljharb avatar Apr 17 '22 20:04 ljharb

@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 }.

hax avatar May 23 '22 21:05 hax

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.)

js-choi avatar Jul 10 '22 16:07 js-choi

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

ljharb avatar Jul 10 '22 16:07 ljharb

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.

js-choi avatar Jul 10 '22 16:07 js-choi

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

ljharb avatar Jul 10 '22 16:07 ljharb

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(^^).)

js-choi avatar Jul 10 '22 17:07 js-choi

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

lygstate avatar Jul 10 '22 19:07 lygstate

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.

js-choi avatar Jul 10 '22 20:07 js-choi

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)

ljharb avatar Jul 10 '22 20:07 ljharb

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.

littledan avatar Mar 14 '23 20:03 littledan