-
Notifications
You must be signed in to change notification settings - Fork 13
Short-circuiting concerns #3
Comments
The pedant in me is compelled to note that in cases where evaluating the LHS is observable the sugar is a little more complicated because you need to avoid triggering the side effects multiple times (though most implementations get it wrong, and do in fact trigger a subset of the side effects several times). No one should care about this, though. |
On this, both Ruby and CoffeeScript have mathematical operators that always assign, and logical operators that short-circuit assign.
Meaning the mathematical assignment operators? but none of them could ever have short circuiting semantics. I think the better argument for always-set semantics path is that it's closer to (my personal intuition) the common de-sugared form ( But, I'm open to either semantics.
This is separate from short-circuiting semantics, but I intend for this to follow whatever the rest of the spec uses, which I hope accounts for deep properties and computed keys. obj.a.b ||= 'test';
(_a = obj.a).b || (_a.b = 'test');
obj.a[key++] ||= 'test';
(_a = obj.a)[_key = key++] || (_a[_key] = 'test'); |
Sorry to comment here late and long. But I am not so familiar with the background of the JS spec and who is allowed to express thoughts here. And please forgive if this is a FAQ, I do not know where and how to look for such. However I'd always vote for the most performant way to implement things if this does not break mathematical correctness. This also means, to allow engines to skip calls to setters if those calls look invariant (which might not necessarily be procedurally correct then) to the value. So my idea is to turn it around and allow shortcuts like in the
which skips calls to the setter if nothing changes. (Note that this should not be imperative, because there will be many cases where calling the setter is more performant than implementing the identity check. And I am talking about performance here.). Now
which effectively is
To keep the idea that The motivation behind this is that setters should do the obvious thing. Storing the same value multiple times or only store it a single time should not catastrophically change things from a stability point of view. (Also getters should never return a value different from what setter expect to set the value returned by the getter again.) On JS this might break some constructs of Observables, which use such a bad approach to enforce some UI updates/refreshes. However for this a clear call to something like Note:
Compare:
|
But what if I want to have: let hasErrors = testForSomeErrors(foo, logger);
hasErrors = testForOtherErrors(foo, logger) || hasErrors; So that |
@hilbix For information, the current state of the spec is: Implementations are allowed to do any optimisation provided that it does not change the observable behaviour, which is probably much more restrictive than what you want. Number of wasted CPU cycles does not count as observable; but observable side-effects not related to time and memory do count. If you want to allow implementations to do more optimisations, that might change the observable behaviour in edge cases, this is not the place to propose it, because it is a major change in the language semantics. See https://github.com/tc39/ecma262/blob/master/CONTRIBUTING.md#new-feature-proposals for places where you can express your ideas (the es-discuss mailing list is one place to discuss them). (Although I am personally sceptical that this particular idea will fly , because the tendency is to reduce observable variations between implementations.) |
I disagree with this argument:
for the simple reason that descriptions like “ In fact, if we look at real semantics instead of simplistic expansions, there is absolute consistency. Here is a synoptic view of the differences of semantics between
The question is whether the “assign the final result to Concretely, the only case where the difference is observable, is when assignment does something weird or perform some non-trivial work. E.g. previewElement.innerHTML ||= '<i>Nothing to show</i>' In that specific case—and I expect in most cases—you most probably don’t want to trigger the |
@claudepache The |
" The problem is, when programmers see
We could also say, if you do not want to trigger the setter, you'd better be more explicit --- write In which direction more explicit is better choice? I don't know. Some programmers think
Whichever we choose, it's not a stylistic choice anymore. So the most conservative teams might even choose enforcing explicit for both directions, aka. disallow logical assignment operators (via eslint). Note, actually I will choose current short-circuiting semantic if I'm forced to choose one. Just feel it's really an issue if the feature is added while a lint rule is also added to forbid it 🤓 |
This. |
The doc of ESLint can continue to lie, and say that In rare cases it makes a noticeable difference, and:
|
I’ve seen far too much horrible, buggy code in my life that this sadly feels very unlikely to me. |
If a language is created for programmers who do not care, then the language will not take care of the programmers who do care. |
Closing this, as I feel #3 (comment) answers everything. |
Could we add a link to this discussion in README which can help the programmers understand the decision if they have similar question? I also think we'd better "fix" the doc of eslint sometime :) |
Is this actually a great example? It feels very off to me. The idea that this doesn't set a side effect is cute and clever, not something that'd be apparent to anyone familiar with a += b desugar into a = a + b but have a ||= b desugar into something like a || (a = b) |
@DanielRosenwasser The fact is, |
What else would // obj evaluated twice in "desugaring"
obj.counter += 1;
obj.counter = obj.counter + 1;
// key incremented twice in "desugaring"
array[key++] += 1;
array[key++] = array[key++] + 1; There's already a difference in the how |
The important part tho is that all the |
For math operators, what else could we have chosen? Of course they always set. The logical operators are control flow. They allow us to have a branching behavior to prevent the set. And I'm willing to bet that the majority of setters don't actually need to be re-set to the same value, and it's possibly very destructive (DOM APIs). |
Having spoken to some friends on the C# team, I found out that they've also picked the behavior of short-circuiting the assignment ( |
Excellent, thank you for reaching out to them! |
There will be much user overhead to know that The primary difference: // Heavily simplifed example from a library
const transaction = {
_currency: 0,
get currency() {
return this._currency;
},
set currency(val) {
if (val !== this._currency) {
// Trigger a cancellable event, that may override the change
console.log(`.dispatch(CURRENCY_CHANGE_EVENT, {old: this._currency, new: val);`);
}
this._currency = val;
// Trigger a non-cancellable event, used for mandatory code auditing, invoking third-party libraries and other service integrations.
console.log(`.dispatch(CURRENCY_SET_EVENT, {val: val);`);
}
};
const CURRENCY_EURO = 978;
transaction.currency = transaction.currency || CURRENCY_EURO;
// .dispatch(CURRENCY_CHANGE_EVENT, {old: 0, new: 978);
// .dispatch(CURRENCY_SET_EVENT, {val: 978);
transaction.currency = transaction.currency || CURRENCY_EURO;
// .dispatch(CURRENCY_SET_EVENT, {val: 978);
transaction.currency ||= CURRENCY_EURO;
// .dispatch(CURRENCY_CHANGE_EVENT, {old: 0, new: 978);
// .dispatch(CURRENCY_SET_EVENT, {val: 978);
transaction.currency ||= CURRENCY_EURO;
/*
* Auditor is not executed.
* Region based runtime added third-party module, data is not yet synced, currency is not set.
* Mandatory attempted change of information process is not executed.
*
*/ |
That’s already something you have to know - i can already always check a property’s value before conditionally setting it. |
@icywolfy If you need to log/report when I think part of the confusion stems from the fact that all existing assignment combo operates are effectively written backwards, Also I wouldn't be comparing this to assignment combo operators. Thearetically the language could support prepending For example: There is obviously less demand for these shorthand operators, but they bring out the point that this has different semantics than assignment combo operators. |
|
As a person who was on that side of the fence, I'd just like to raise:
|
(raised by @bakkot in the meeting today)
Currently, every assignment combo operator in JS can be described as
a = a <op> b
being equivalent toa <op>= b
.Including the short-circuiting semantics would make
a = a <op> b
equivalent toif (a) { a = a <op> b; }
, which is a very large inconsistency.The argument that "Ruby and CoffeeScript have these semantics" imo does not hold its weight compared to "it would be inconsistent with the rest of JS".
The text was updated successfully, but these errors were encountered: