Is the binding visible in the `else`?
Moving from #2.
Consider
if (const a = Math.random() < .5) {
console.log(a); // definitely works
console.log(b); // definitely throws
} else if (const b = Math.random() < .5) {
console.log(a); // works?
console.log(b); // definitely works
} else {
console.log(a); // works?
console.log(b); // works?
}
What should the three works? lines do?
For background, in C++,
#include <iostream>
using namespace std;
int main() {
if (bool a = false) {
cout << a << endl;
} else if (bool b = false) {
cout << a << endl;
cout << b << endl;
} else {
cout << a << endl;
cout << b << endl;
}
return 0;
}
is a legal program.
From the last line of the Overview:
... and the variable would only be visible inside the if/while block.
if (const a = Math.random() < 0.5) {
console.log(a); // works
console.log(b); // throws
} else if (const b = Math.random() < 0.5) {
console.log(a); // throws
console.log(b); // works
} else {
console.log(a); // throws
console.log(b); // throws
}
As mentioned in #2, this is what makes sense to me, but I could be convinced otherwise. I don't feel particularly strongly either way, other than that it seems weird to have a variable be visible across multiple { ... }.
other than that it seems weird to have a variable be visible across multiple
{ ... }
That's the normal behavior of variables: a variable declared in some outer scope is visible to all nested scopes, not just the immediately following one.
I don’t believe almost anybody considers an else or an else if as a nested scope.
Although this isn't "technically" correct, when I see something like:
if (let data = foo.data) {
...
}
I think that data is actually "part" of the { ... }, not outside it. If I declared data outside of the if ( ... ), then yes, it would be visible in both { ... }, but declaring it inside the if ( ... ) makes my mind see it differently.
I don't consider the else (or else if) to be nested inside the if. I see it as a completely different scope that's only related because it's easier to write else than another if with an inverse condition.
I realize that others may see that differently, however, which is why I'm open to alternative ideas, but I'd like a good reason for making it visible inside the else other than "that's how C++ does it".
To be clear, this is not meant as a criticism of you or your ideas, but just how I'm approaching this particular idea.
I think that data is actually "part" of the
{ ... }
if without {} :
if (let data = foo.data) process(data)
else console.log(`data is falsy: ${data}`)
And as future work
if (let x = 1, y = 2; x || y) {
/* ... */
}
it's likely there will be cases programmers want to access x or y in else clause.
But If we really make x visible in else,
let x = ...
if (let x = foo.data) {
...
} else if (let x = bar.data) {
...
} else {
console.log(x) // a little bit confusion, I guess some will expect outer x
}
Another point is we'd better coordinate with try (let x = ...) {} catch {} finally {} which currently seems do not allow x be visible in catch/finally clauses.
Not extending the scope to the else clause would be highly problematic for a couple reasons:
- Incompability with desirable future extensions such as
if (let x = foo(); bar(x)) { ... } else {console.log(x)} - Gratuituous subtle divergence from C++, making it hard to remember which one is which
Note that C++17 includes both forms if (Foo x = ...) and if (Foo x = ...; condition) and extends the scope to both the then and else clauses.
@waldemarhorwat ~~I think python has issues of for...else feature (https://mail.python.org/pipermail/python-ideas/2009-October/006155.html) and I very doubt it could be success in JS. And~~ I don't think C++ consistency should have very high priority.
But if I'm forced to choose one side, i will support extending the scope!
There's no way this feature pays for itself if the scope includes both consequent and alternate.
if (let a = b()) {
// ...
} else {
// ...
}
as sugar for
{
let a = b();
if (a) {
// ...
} else {
// ...
}
}
is not a big win for the added language complexity. And limiting the scope to just the consequent brings with it other problems, including the divergence from C++ and confusion with "through" references (references that would have been shadowed if the scope included the alternate). I'd like to stress that I consider myself to be squarely in the target demographic for this proposal, as I am always trying to minimise the scope of my variables, my try/catch statements, etc when writing JavaScript.
Adherence to C++ is irrelevant unless you're a C++ programmer; if there's good reasons to diverge, we should do so.
@ljharb Of course, that is good general practice, but we should not ignore that the proposal was presented as essentially "I am a C++ programmer and I found it surprising that JS does not have this feature". Implied in that is that the feature should be familiar for C++ programmers. It's not fatal if it fails that requirement.
Moreover, alignment with C++ here equates to divergence from Rust, Swift, etc.
I write C++ for my job and make use of this construct, but I'm shocked to learn that it does diverge from modern languages on this point. :flushed: (A coworker pointed out that it could be useful when dealing with C-style return codes; this makes sense for C++, but I don't think this applies much elsewhere...)
I strongly agree with @michaelficarra that this feature doesn't pay for itself if it's just if-in-a-block.
From @michaelficarra's comment:
@ljharb Of course, that is good general practice, but we should not ignore that the proposal was presented as essentially "I am a C++ programmer and I found it surprising that JS does not have this feature". Implied in that is that the feature should be familiar for C++ programmers. It's not fatal if it fails that requirement.
I apologize if that was how I portrayed it. It's not my intention to make this identical to C++. In fact, I didn't realize that C++ makes the variable visible in the else until recently, and I personally have yet to find that useful at all. That is one of the reasons why I initially proposed not doing this for JavaScript (variable is only visible inside if).
What should the three
works?lines do?
- throws
- throws
- throws
@Mouvedia in strict mode, as opposed to throwing a reference error?
Well I don't know if a and b were declared before but yeah either output the outer value or throw.
He's using const so a redeclaration would throw anyway. So these are implicitly/probably not declared.
@ecyrbe not from language usage - and since there’s no way to observe either, it’s up to individual interpretation.
If this proposal is a nerfed form of pattern matching, i would object to it advancing. This proposal, to me, is “declare a binding in the if condition, for use in the if block”. If you want to use a variable in two blocks (inside two adjacent sets of curly braces) you should always need to declare a variable outside both sets of curly braces.
@ljharb The declaration is outside both sets of curly braces. If that's the rule, it should be available in both.
@bakkot the rule is also (arguably) that things declared in attached parens don't make it available outside the immediately adjacent block:
try {
} catch (e) {
} finally {
// no e available here
}
Why is that not the case for if/else clauses? The else runs when the if condition is falsy; also, e can be undefined in the catch block.
I can't believe we have to explain these details. Are you really a js teacher ?
Personal attacks are not acceptable communication. See https://tc39.es/code-of-conduct/
The declaration is outside both sets of curly braces. If that's the rule, it should be available in both.
Yes!
Why is that not the case for if/else clauses? The else runs when the if condition is falsy;
Personally, catch is legacy. It's not a catch (let e) {}, it's more like a param catch(e) {}. I'm willing to let this be a wart with an otherwise consistent feature.
also, e can be undefined in the catch block.
My if (const foo = undefined; !foo) can also have foo === undefined, so I don't understand this significance.
If we were to allow all 3 works? to work, then one could transpile to something like this:
{
const a = Math.random();
if (a < .5) {
console.log(a); // definitely works
console.log(b); // definitely throws
} else {
const b = Math.random();
if (b < .5) {
console.log(a); // works?
console.log(b); // definitely works
} else {
console.log(a); // works?
console.log(b); // works?
}
}
}
However, the untranspiled syntax may not make a lot of sense unless there was a way to declare that you also want to 'share' that variable with an alternative block if it's truthy block is never run. Such a thing might look like this:
if (sharing const a = Math.random < .5) {
console.log(a); // definitely works
console.log(b); // definitely throws
} else if (sharing const b = Math.random() < .5) {
console.log(a); // definitely works
console.log(b); // definitely works
} else if (const c = Math.random() < .5) {
console.log(a); // works?
console.log(b); // definitely works
console.log(c); // definitely works
} else {
console.log(a); // works?
console.log(b); // works?
console.log(c); // definitely throws
}
It may also be more correct for a shared keyword to be better as a followup proposal rather than packed into this proposal.
When declaring variables inside the initialization of a for loop, variables declared using a LexicalDeclaration (e.g. let) are local to the statement.
const cars = ['Ford', 'Mercedes', 'BMW'];
for (let i = 0, len = cars.length; i < len; i++) {
console.log(len); // ~> 3
console.log(cars[i]);
}
console.log(len); // ReferenceError: len is not defined
To me it'd be logic that Declarations in Conditionals follows the same logic.
Well, consider this use:
if ( const a = Math.random(); a < 0.5 ) {
console.log("Number %d is above 0.5!", a);
} else {
console.log("Number %d is not above 0.5!", a);
}
there, I have a reason for wanting the original value of a, but from within the else clause.
In what ways would that be confusing to a JS developer?
Then again, even Rust doesn't allow visibility to the else clause, so Rust might've been a much better example in the readme, as opposed to C++.
@ecyrbe Ah, yes, forgive me, I forgot that there was a proposal to add that to JavaScript. And yes, I agree with Rust's if/let being a form of pattern matching, it checks if the value is in the correct shape, and then uses it as appropriate, otherwise it is unusable.
If the current plan is to deny it to the if scope, then this entire proposal is useless.
You might as well use labels
if ( let x = foo(); undefined !== x ) {
// use `x` however you want
} else {
// no `x`
}
vs
_: {
let x = foo();
if ( undefined === x ) {
break _;
}
// use `x` however you want
}
// no `x`
Maybe one may not be familiar with the syntax, but it's semanticially equivalent in almost every way; to add a different syntax for the same thing, I cannot support that, unless the older method were to be deprecated and removed.