-
Notifications
You must be signed in to change notification settings - Fork 3
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
Passing arguments vs. ignoring arguments #5
Comments
Here's my ideal implementation that wraps up all of my preferences: const once = fn => {
let completed = false, errored = false, storedReturn, storedError;
return () => {
if (!completed) {
try {
storedReturn = fn();
} catch (e) {
storedError = e;
errored = true;
}
fn = void 0;
completed = true;
}
if (errored) {
throw new Error(storedError.message, { cause: storedError });
} else {
return storedReturn;
}
};
} Written differently, const once = fn => {
let impl = () => {
try {
let storedReturn = fn();
impl = () => storedReturn;
} catch (e) {
impl = () => { throw new Error(e.message, { cause: e }); };
}
return impl();
};
return () => impl();
}
|
I was wrong; I misread the code. @michaelficarra’s code caches the result and returns/throws the result every time, which @waldemarhorwat is uncomfortable about. |
Note that otherwise that works fine for me. |
I'm strictly for passing arguments. For example, the case of callbacks - event listeners. Sometime they should be called one time, but they depends on the passed event argument. Ignore of the argument for a such case is a mistake. |
Passing the argument but ignoring it on successive calls when it might be different seems like a mistake also, though. |
It is a mistake blessed by precedent. Removing it will only make it more difficult to adopt the native feature over lodash's implementation. |
It is a good point that once-only event listeners often use their event arguments: el.addEventListener('click', console.log, { once: true }); For what it’s worth, another option is for the new function to store references (weak references?) to its first call’s arguments (and Any subsequent call to the new function would check whether its arguments etc. match its first call’s. If they are identical, then the subsequent call is a no-op. If they do not match, the new function throws. const fn = (function (x) { return }).once();
fn(1); // Prints 1.
fn(1); // Does nothing.
fn(0); // Throws. This approach might be okay—if most subsequent calls on @ljharb: I know that you do not like throwing on every subsequent call (#2 (comment)), but perhaps throwing only on subsequent calls with different arguments might be better: allowing the first call to supply arguments while still preserving idempotence. @jridgewell: It seems like you might agree that it would have been ideal if Lodash had different behavior in the first place, but that we are stuck with what developers are used to. I’ve opened an issue devoted to whether we should prioritize first principles or userland precedent first (#9). |
@js-choi that would be better, but how do you define "different arguments"? |
@ljharb: We would probably SameValue semantics, although SameValueZero may also be reasonable. const fn = (function (x) { return }).once();
const o = {};
fn(o); // Prints 1.
fn(o); // Does nothing.
fn({}); // Throws. const fn = (function (x) { return }).once();
fn(0); // Prints 1.
fn(0); // Does nothing.
fn(Object(0)); // Throws. const fn = (function (x) { return }).once();
fn(0); // Prints 1.
fn(0); // Does nothing.
fn(-0); // Throws? |
Do two calls to the same event handler for the same event and element receive the same event object, or do they receive a distinct object that's conceptually equal? |
No, I believe lodash's behavior is correct, and explicitly write code that depends on its behavior. We could debate error throwing, but passing arguments and caching the first return value is exactly the right semantics.
They're distinct identities. Any memoization would defeat using |
@ljharb: Could you clarify what sort of situation you’re thinking of? I’m assuming you’re talking about event targets in the DOM. Two different clicks will create two different event objects. I’m not aware of any DOM events that may cause the event listener to be called more than once for the very same trigger at the very same time. (Although the DOM has |
yes, that's what i'm talking about. which means that throwing on "different" arguments would cause a onced event listener to always throw after the first call. |
Yes, that is right; there’s just no way around the dilemma: If we want to pass the arguments, we either must either completely ignore them on subsequent calls (and possibly cause “misleading” behavior when calling with different arguments?) or throw on subsequent calls with different arguments (including nonidentical event objects). @ljharb, you mentioned that using the first call’s arguments but then completely ignoring subsequent calls’ arguments might seem like a mistake (#5 (comment)). But, even with those misgivings, would you agree that that’s what we need (An aside: I already mentioned that DOM EventTargets already have |
I agree that since events already have a "once", we shouldn't be too concerned with them. |
Yes, and to continue pointing at the real-world examples in the explainer, we should look at what real-world scenarios other than EventTargets/EventEmitters would actually use their first calls’ arguments. With the exception of the glob snippet, which uses a Node EventEmitter (and which arguably should be removed from the explainer), every one of those explainer real-world examples uses a nullary callback. Therefore, research probably needs to be done: we need to search for real-world examples of once functions with unary or n-ary callbacks that actually use their arguments—and which aren’t already handled by DOM EventTargets or Node EventEmitters. (Note: The real-world examples currently in the explainer were obtained by selecting some of the most-downloaded libraries that were dependent on various once-function libraries like onetime and lodash.once.) |
michaelficarra wrote:
@michaelficarra: Did you notice that these two "ideal" implementations have observably different behavior? Which one did you intend? (I prefer the second one) |
@waldemarhorwat No, I didn't. What's an example where the output/effects differ? I'm sure either impl would be fine with me. |
Ignoring the issue of what value to return (#2), I’d like to revisit caching the arguments of the first call and checking them on any subsequent call. This would ameliorate @waldemarhorwat’s pitfall in #2 (comment). function once (callback) {
// This variable is undefined before callback is called.
// After the first call, the variable is an array that caches the
// this-receiver, new.target, and arguments of the first call.
let previousCall;
return function callOnce (...args) {
const currentCall = [ this, new.target, ...args ];
if (previousCall) {
const callsAreIdentical = (
currentCall.length === previousCall.length &&
currentCall.every((a, i) => a === previousCall[i])
);
if (callsAreIdentical) {
/* Return undefined or return a cached result; see issue #2. */;
} else {
throw new Error(
`A once function was given different arguments than its first call.`,
);
}
} else {
previousCall = currentCall;
callback.call(receiver, new.target, ...args);
/* Return undefined or cached the result and return it; see issue #2. */;
}
};
} In other words: function f(x) {return x*x;}
const fOnce = f.once();
fOnce(4); // Doesn’t throw. Might return 16 or undefined; see issue #2.
fOnce(4); // Same.
fOnce(7); // Throws an error because zeroth argument is not 4. |
What semantics would you use for caching the arguments? What if i call it with a mutable object, and then mutate the object later, and pass the same object, expecting different behavior? |
@ljharb: I was using |
If we cache the result of the first call (see #2), then subsequent calls’ arguments might be different and might not match those of the first call. Although this is what most userland libraries do today, @waldemarhorwat pointed out that this may be an antipattern. So our possibilities are:
undefined
every time.undefined
every time.At the plenary meeting today on 2022-03-29, most representatives generally called for not passing arguments and returning
undefined
every time (see #2).We need to do research to see if there are any use cases for non-nullary “once” functions that are not event handlers. Non-nullary “once” event handlers may be better covered by devoted methods or options on the target, such as DOM EventTarget’s addEventListener’s
{ once: true }
option or Node EventEmitter’s.once
method. All of the real-world use cases I can find so far involve nullary callbacks.The text was updated successfully, but these errors were encountered: