Open js-choi opened 2 years ago
Why don't we have both?
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
.
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);
I'm for Function.prototype.once
for consistency with .bind
. In case of adding static equals, they should have different names.
Adding static
Function.bind
will be a breaking change. I saw (and used)Function.bind
as a shortcut forFunction.prototype.bind
too many times…
Ah, yeah, silly me. I had forgotten that the Function
constructor itself is a function…
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.
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. 🤪
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() { ... }
Yeah, consider decorator advanced to stage 3, I hope we could revisit function decorators in future meetings.
I'm strictly for a prototype method since it's simpler for usage in most cases.
Vote for Function.once
only, like Object.hasOwn,
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.
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.
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 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 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 }
.
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 usefoo[Symbol.decorator]
, andFunction.prototype[Symbol.decorator]
could beget [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 function executeEffect() {} |> Function.once(^)
, also - the only benefit of a decorator there is on a function declaration.
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.
Yes, that's what i meant - they'd do const executeEffect = Function.once(function executeEffect());
, or via pipeline.
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(^^)
.)
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)
thanFunction.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
andonce
is quite different, and that.bind
should not be used as a strong precedent on the form ofonce
.
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
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.
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)
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.
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
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.