Open jorendorff opened 6 years ago
The developer console has the same semantics, and I’d argue that most programmers do know/learn that in practice.
Surely you're not arguing that programmers already know that book.title; break;
is a useful thing that you can write in a loop, because of their experience with the developer console.
The completion values of loops have been discussed elsewhere. The completion value of behavior of every other statement with the exception of function or class declarations are, in my opinion, very intuitive.
Well, maybe we just disagree, but I was born without any real intuition for whether eval("f(); var x = g();")
should return the value of f()
or the value of g()
. (It's f()
.) Or whether break
should propagate the "last value" or set the loop's value to undefined (for break
in particular, it's the former, but note that for exceptions it's the latter: a catch block effectively resets the "last value" to undefined before it runs). Or how ExpressionStatements in finally
blocks should affect the value of a try/finally
(usually they don't, but if the finally
block exits with a break
or continue
, then they do).
All that stuff seems pretty arbitrary, to me. Maybe in practice it just won't come up. I agree it's loops and declarations that will actually surprise people the most often.
It would be clearer if do-expressions had an explicit syntax to mark the produced value, à la return
and yield
:
let name = do {
for (const book of books) {
if (isRecommended(book)) {
use book.title;
}
}
};
@claudepache that misses the point of do expressions almost entirely.
Here's a fun case. Empty Blocks usually don't affect the value:
eval("3; {} {} {}"); // returns 3
But if you "comment one out" using if (false)
, then it does:
eval("3; {} {} if (false) {}"); // returns undefined
I love it: an empty statement has no side effects, thank goodness—unless you make it not execute...
eval("3; {} {} if (false) {}"); // returns undefined I love it: an empty statement has no side effects, thank goodness—unless you make it not execute...
It isn't the not executed block that has a side-effect (the completion value) it is the if
statement that has a side-effect (completion value)
This is part of ES6 "completion reform" championed originally by @dherman. Essentially completion reform was intended to make completion values easier to understand/analyze by ensuring that all statement list elements either always produce a normal completion value or never produce a normal completion value I(they just propagate the previous completion value).
Previously 0; if (cond) 1;
would sometime produce a new completion value (1) and sometimes propagate the previous completion value (0). There was no way to statically determine which it would be.
Thanks for the background, Allen.
As long as [[Type]]: break, [[Value]]: empty
completion values are combined with the results of previous statements, the determination isn't exactly static, though. It's static in some places and dynamic in others. :-\
Edit: I don't mean to argue with anyone or blame anyone by saying that. Just a bit stuck for how to proceed from here...
Well break
produces [[Type]]: break, [[Value]]: empty
but StatementList propagates non-empty completion values into any break completion records emitted by the StatementList and that completion record would just pass through a containing Block. Any surrounding StatementList would then propagate its current non-empty completion value into that break completion record in the same way.
You're right, it's more static than I thought.
Is the idea that in {1; 2 && do { break; }}
the second ExpressionStatement would complete with [[Value]]: undefined
rather than [[Value]]: empty
? (That is the kind of situation I had in mind.)
That's the behavior I'd expect from that particular code.
For {1; 2 && do { break; }}
the second ExpressionStatement's completion value would be [[Value]]: empty
because expressions do not propagate completion values among sub-expressions. The completion value for the block would be [[Type]]: break, [[Value]]: 1
because the completion value of the first statement has propagated into the empty-valued break completion of the second statement.
So: {1; 2 && do { break; }}
is equivalent to (1; {break}}
which is equivalent to {1; break}
So value && empty == empty
? Does that apply to every operator? Like what if I had
1; condition ? do { break; } : do { 4; }
So: {1; 2 && do { break; }} is equivalent to (1; {break}} which is equivalent to {1; break}
Perhaps a better equivalence:
{1; 2 && do { break; }}
is equivalent to:
{1;
if (2) {break}
}
is equivalent to:
{1;
if (2) break
}
So value && empty == empty? Does that apply to every operator?
This is where some new design is required. Currently (I believe) there is no way for a subexpression to evaluate to [[Type]]: normal, [[Value]]: empty
so this isn't a situation the spec. currently deals with. But with do expressions we have the possibility of things like (1 + do {})
.
The do expression do {}
needs to evaluate to [[Type]]: normal, [[Value]]: empty
so it has Tennant's correspondence with { }
when used in a statement context.
So, here is how we might achieve this.
Let's assume for now that subexpressions that evaluate to [[Type]]: normal, [[Value]]: empty
should be consider to have the value undefined
by expression operators (this could be separately debated). Well, expressions that need ECMAScript values always call the abstract operation GetValue on the result of evaluating subexpressions. So, we could update GetValue so that it returns undefined
if passed a completion [[Type]]: normal, [[Value]]: empty
.
But, we would also need to look carefully at all the uses of GetValue (and there are a lot) to find any cases where we wouldn't want to do that conversion. There is one that immediately comes to mind: ExpressionStatement evaluation:
To maintain Tennant's correspondence with Block it would need to change to:
Just to clarify: it seems like in this comment you're changing your mind, saying that the result should be undefined, not 1. Right?
If so, I think I agree this is the only design consistent with completion value reform. Without this, we're right back in the situation described here:
Previously
0; if (cond) 1;
would sometime produce a new completion value (1) and sometimes propagate the previous completion value (0). There was no way to statically determine which it would be.
because there would be cases like 0; (cond ? 1 : do{});
.
Heh! The very first instance of GetValue() in the spec is in array initializers. I assume we want [0, 1, do{}, 3]
to have an element 2 with the value undefined, not a hole...
Design decisions around every corner!
Interesting, referring back to the spec. I see that the behavior of if
seems to have changed between ES2015 and the current ES2018 draft:
In 2015:
{ 1;
if (true) break;
}
produces the completion value for the block: [[Type]: break, [[Value]]: 1
and
{ 1;
if (true) ;
}
produces the completion value for the block: [[Type]: Normal, [[Value]]: undefined.
In the ES2018 draft, they respectively produce:
The latter is consistent with what Jason is saying. However, the algorithm conventions for manipulating completion results changed between those versions of the spec. It isn't obvious to me that the semantic change was intentional or a bug introduced when rewriting using the new conventions. It probably should be researched to see if the 2015 behavior had been identified as a bug and the change was an intentional fix.
The bigger question is what (if anything) was the intent of "completion reform" for cases like this. It's pretty clear that a goal of completion reform was that during linear progress, each statement either always or never produces a new completion value. But abrupt completions are really a separate dimensions of completion value propagation, so it isn't obvious that the always/never rule should apply to it.
In seems strange to me that:
{ 1;
break;
}
would have a completion value of [[Type]: break, [[Value]]: 1
But,
{ 1;
if (true) break;
}
has a completion value of [[Type]: break, [[Value]]: undefined
I agree!
But consider:
while (true) {
1;
if (cond) break;
break;
}
In the current draft, the value of this block is undefined either way.
What did it do in ES2015? I think it would be 1 if cond is true, and undefined if cond is false. This seems strange.
So both semantics have some strange cases. I think if I could pin down what we mean by "static" here, I would understand better where the strangeness is coming from.
So, the if behavior was a ES2016 breaking change https://github.com/tc39/ecma262/issues/1085#issuecomment-362347581
I think pretty much all of the trivia goes away if we change abrupt break and continue completion so that they always have [[Value]]: undefined.
That would of course be a breaking change. It would make the rules a snap to reason about, though:
It's easy to see statically which statement values matter and which don't:
When a nonempty Block or StatementList terminates normally, its value is always the value of the last nonempty statement in it.
The values produced by all the other statements, when they terminate normally, never matter.
The value of a loop that terminates normally is always the value of the last statement inside the loop, or undefined if the last pass through the loop exited with break
or continue
.
The value of any Statement or LabeledStatement that is terminated normally by break
-ing out of it is always undefined.
Oh, I guess actually getting the above rules would involve changing var
and let
to produce undefined
as well. Further breakage. Likely too much... I guess we could try it.
Strictly speaking, we're not required to use the same completion value semantics that eval
uses. The argument in favor is simplicity of mental model, but to be honest I'm not too concerned with the simplicity of the mental model of people who are using eval
to get completion values. (I'm also not totally sure that any such people exist.)
Another fun example of how evaluating to the last expression will generate a lot of confussion
const result = do {
switch (val) {
case 'foo': 1;
case 'bar': 2;
}
}
What does result
eval to?
Intuitively, the only options are either “always undefined”, or “1, 2, or undefined, based on val
” imo - and you could verify it by sticking the body of the do expression in a repl or in eval.
What confusion am i missing?
@ljharb, I assume the point is that there's no break
, so it can never be 1
. I agree this seems like something which would be a common error.
Aha, fair - although that’s a hazard of switch statements themselves, and not something new for do expressions.
Intuitively, the only options are either “always undefined”, or “1, 2, or undefined, based on val” imo - and you could verify it by sticking the body of the do expression in a repl or in eval.
What do you mean by intuitive? The idea of having to insert code into a repl in order to know what it does seems like a pretty bad experience as a developer.
Aha, fair - although that’s a hazard of switch statements themselves, and not something new for do expressions.
This is not a hazard for switch statements, it just shows a limitation in the proposal, being unable to resolve to a given expression without any ambiguity leads to a number of possible mistakes, take for instance the extremely failed with
javascript keyword, deprecated because of its immense ambiguity.
There isn’t any ambiguity with switch; it’s just not intuitive, because switch itself in JS is an unintuitive construct.
@eloytoro We have eslint, and this kind of code will be warned.
For me, do expression helps me to build template engine that non-coder can understand and use. Non-coder may be difficult to understand let and const, but they can easily understand "oh value inside { } is just what I will get"
Eslint Is not part of the spec, it can't be used as an argument for negligence
On Fri, Apr 27, 2018, 8:34 AM lin onetwo notifications@github.com wrote:
@eloytoro https://github.com/eloytoro We have eslint, and this kind of code will be warned.
For me, do expression helps me to build template engine that non-coder can understand and use. Non-coder may be difficult to understand let and const, but they can easily understand "oh value inside { } is just what I will get"
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-do-expressions/issues/21#issuecomment-384971505, or mute the thread https://github.com/notifications/unsubscribe-auth/ADwSBX5yJ8OA9hR5YGC_fAnMFHrNB_YSks5tsx5-gaJpZM4RjL_Y .
One possible solution for this is to syntactically require that the last statement in the do
block be an expression.
Or, at least, not a loop or declaration.
As it stands, you can be a very good JS programmer and not even know that statements produce values.
If
do
expressions are added, programmers will have to understand the rules in some detail:In the spec, the way these statements produce a value is all faux-functional, but to programmers, that
book.title
is implicitly stored in a nameless variable, and implicitly read out of it later. That's spooky—and while it's trueeval
already has all these rules baked into it today, in practice nobody has to know.