proposal-Declarations-in-Conditionals icon indicating copy to clipboard operation
proposal-Declarations-in-Conditionals copied to clipboard

Is the binding visible in the `else`?

Open bakkot opened this issue 6 years ago • 24 comments

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.

bakkot avatar Sep 25 '19 18:09 bakkot

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 { ... }.

dcrousso avatar Sep 25 '19 18:09 dcrousso

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.

bakkot avatar Sep 25 '19 18:09 bakkot

I don’t believe almost anybody considers an else or an else if as a nested scope.

ljharb avatar Sep 25 '19 18:09 ljharb

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.

dcrousso avatar Sep 25 '19 18:09 dcrousso

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.

hax avatar Sep 30 '19 16:09 hax

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 avatar Oct 02 '19 19:10 waldemarhorwat

@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!

hax avatar Oct 02 '19 19:10 hax

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.

michaelficarra avatar Oct 02 '19 19:10 michaelficarra

Adherence to C++ is irrelevant unless you're a C++ programmer; if there's good reasons to diverge, we should do so.

ljharb avatar Oct 02 '19 19:10 ljharb

@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.

michaelficarra avatar Oct 02 '19 20:10 michaelficarra

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.

rkirsling avatar Oct 02 '19 20:10 rkirsling

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).

dcrousso avatar Oct 02 '19 20:10 dcrousso

What should the three works? lines do?

  1. throws
  2. throws
  3. throws

Mouvedia avatar Nov 08 '19 19:11 Mouvedia

@Mouvedia in strict mode, as opposed to throwing a reference error?

ljharb avatar Nov 08 '19 19:11 ljharb

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.

Mouvedia avatar Nov 08 '19 19:11 Mouvedia

@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 avatar Nov 14 '19 17:11 ljharb

@ljharb The declaration is outside both sets of curly braces. If that's the rule, it should be available in both.

bakkot avatar Nov 14 '19 17:11 bakkot

@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
}

ljharb avatar Nov 14 '19 17:11 ljharb

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.

ljharb avatar Nov 18 '19 19:11 ljharb

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.

jridgewell avatar Nov 18 '19 19:11 jridgewell

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.

puppy0cam avatar Nov 19 '19 04:11 puppy0cam

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 &lt; 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.

bramus avatar Aug 04 '20 20:08 bramus

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++.

ghost avatar Jan 12 '21 21:01 ghost

@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.

ghost avatar Mar 03 '21 05:03 ghost