-
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
Subsequent calls: cache, undefined, throw, or both #2
Comments
I think caching is what everything in the ecosystem does? (Underscore/lodash, at least.) Probably best to match that behavior. For other behaviors, if we think they are important enough to support (which I am not offering an opinion on), I think we could reasonably have an argument, e.g. |
SugarJS and some other libraries already provide |
I think this would be useful, alternatively we could even provide a function to let users decide i.e.: fn.once({
subsequentCalls: () => {
console.warning("Resource has already been cleaned up, consider releasing the object earlier");
},
});
fn2.once({
subsequentCalls: () => {
throw new Error("value has already been extracted and can no longer be consumed");
},
});
fn3.once({
subsequentCalls: () => {
// Value not available so return null
return null;
},
});
fn4.once({
// Maybe even allow for caching first return for later use
cacheFirstReturnValue: true,
subsequentCalls: (fd) => {
throw new Error(`File descriptor ${ fd } is now stale`);
},
}); If we did this, the only behaviour we would need to specify is what is the default for "subsequentCalls". |
Allow specifying function value for "subsequentCalls" seems too powerful. Simple argument like |
The reason it's more useful to have a function, is you can't give any useful info about "why" calling more than once is an error. i.e. The examples above would allow you to customize the error to indicate why. |
I just realize this issue is much tough than I thought. I agree with @bakkot, Option 0 (cache) behavior seems the best match most ecosystem lib. And I don't like silent failure like Option 1 ( I also notice that there are issues of throwing. For Option 0/1, does the throwing also be cached? For Option 2, how to differentiate the reason of throwing? |
Yes, the error-caching question is a good one. We need to investigate what current libraries do here. |
Caching functions might be ok on functions that don't take any arguments but is inappropriate on functions that do take them.
If you wanted
Another problem is that caching will deadlock or fail if the body of |
@waldemarhorwat: It does look like at least one See also #3. |
At the plenary meeting today, several representatives (@waldemarhorwat, @ljharb) seemed to support returning The argument is that #5 makes caching the first result into a pitfall and an antipattern, and that This would also probably necessitate that this be renamed to Function.once (#1, #4). |
I strongly support option 1. Option 2 sucks because you want to be able to pass your onced function elsewhere, and you can't control what arguments it's called with. Option 0 is indeed the precedent, but I would be very surprised if the wider ecosystem was even aware that Option 3 seems not particularly valuable. If we want a memoize helper, I think we should indeed add one separately. |
(For what it’s worth, I am willing to champion a separate, configurable |
From my comment which links to 9 different in-the-wild implementations: #4 (comment)
Of note is the return function(){
return ++i <= 1 && fn.apply(ctx || fn, slice.call(arguments));
}; |
I'm strictly for the cached result. |
@keithamus |
My code depends on this behavior. Moreover, it's explicitly written in eg. Lodash documentation. https://lodash.com/docs/4.17.15#once |
It's in the prose, but in none of the code examples (same for underscore). I'm sure there are a number of people that do use it this way - I'm just confident it's a very very small number. |
I'm confident it's a very very big number. For example, the cached result in this case is a kind of the singleton software design pattern. It's used in most popular libraries like |
Are there any use cases y’all are aware of that take arguments? Or are you only using |
For example, callbacks - event listeners? Some time they should be called one time, but they depends on passed |
In my use cases it was always zero-arg memoize. |
I think that we should be matching lodash's cache and arg passing behavior exactly, and I have written code that depends on it. |
For what it’s worth, I have also created a proposal-function-memo, which might cover singleton use cases, and which I plan to present to a future plenary. It’s very unspecified right now, being at Stage 0. |
I think we should always start from first principles, and consider userland precedent second to that. |
If caching is on the table, it might be prudent to explicitly ignore arguments, in order to guarantee same behavior. If arguments are allowed, the caching approach is either going to be wrong or correct-by-coincidence. Background: I've used 0-arg cached functions quite a few times in our codebase, using a custom |
@ckknight: Indeed, there is an issue devoted to this question, #5. 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 |
I was expecting "always-undefined", but if the ecosystem has found that caching the first answer is more convenient, then that's fine with me too (it does seem like a 0-ary memo in that case, though the caching policy is more straightforward to decide on). |
Let’s say we define:
We have four options with
once
.Option 0: Cache and return cached result
Cache and return the value of the first call from any subsequent calls. This may cause the created function to behave more predictably.
Sync errors thrown by the first call may either be cached and thrown by every subsequent call…or thrown only once.
We could pass arguments from the created function’s first call to its callback—or we could ignore arguments. There is an issue devoted to this: #5.
Option 1: Always return undefined
Return undefined from any subsequent calls. The created function’s return value may become less predictable, but it does let the developer more easily detect when a call is not the first call.
Sync errors thrown by the first call would be thrown only once.
Option 2: Always throw on subsequent calls
Throw an error after any subsequent calls. I would personally not expect this as default behavior.
Sync errors thrown by the first call would be thrown only once.
Option 3: Multiple functions
Split off the Throw option into its own “strictOnce” function. Several existing libraries offer this. I’m neutral about it.
Sync errors thrown by the first call may either be cached and thrown by every subsequent call…or thrown only once.
The text was updated successfully, but these errors were encountered: