Skip to content
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

Destructuring declarations that bind nothing should probably be an early error #97

Closed
allenwb opened this issue Oct 21, 2015 · 66 comments
Closed
Labels
needs consensus This needs committee consensus before it can be eligible to be merged. normative change Affects behavior required to correctly evaluate some ECMAScript source text spec bug

Comments

@allenwb
Copy link
Member

allenwb commented Oct 21, 2015

In the ES6 spec., there is no check that a destructuring let/const/var actually introduces any bindings.

For example:

let ;

is a syntax error, but (updated 10/22/15)

let { } = obj;

is valid syntax that does nothing (it doesn't add any new variable bindings).

There are reports that some ES6 developers are encountering bugs cause by complex destructing patterns that unintentionally don't introduce any bindings.

This seems like a design bug in ES6. It would be trivial to specify that this is an early syntax error.

In 13.3.1.1
LexicalBinding : BindingPattern Initializer
- It is a Syntax Error if the BoundNames of BindingPattern has no elements.

In 14.1.2
FormalParameter : * BindingElement*
- It is a Syntax Error if the BoundNames of BindingElement has no elements.

Note that this is a breaking change for any program that actually contains such empty binding patterns. So there is some risk to making this change.

@allenwb allenwb added normative change Affects behavior required to correctly evaluate some ECMAScript source text spec bug labels Oct 21, 2015
@bterlson
Copy link
Member

On the surface fixing this bug seems like something we could get away with while there are no interoperable destructuring implementations. Nice find!

@nzakas
Copy link

nzakas commented Oct 21, 2015

Oops, just sent an email to esdiscuss before I saw this. Yay! :)

@bterlson
Copy link
Member

@nzakas thanks much for pointing this out :) Definitely file bugs for any other issues you find!

@bterlson
Copy link
Member

@allenwb what about non-binding forms (AssignmentPattern and friends)? Should [] = [] be an error? I say so...

@bterlson
Copy link
Member

I also note that let {} and let [] are syntax errors at least since the initializer is not optional.

@getify
Copy link
Contributor

getify commented Oct 22, 2015

I have strong concerns about this proposal:

  1. I have on two occasions already had the need (during debugging, etc) to do things like this:

    var {
    // x,
    // y
    } = foo();

    ...where I comment out different parts of the destructuring declaration, and in some cases even comment out all of them.

    It would really suck if this practice of commenting them out during debugging and dev would work only while there was at least one left uncommented, but if I needed them all commented out, I'd have to re-arrange the code by commenting out the entire destructuring pattern part, but (for example) maybe still need to call the foo().

    In that scenario, this seems at best surprising and at worst rather hostile.

  2. What about array destructuring? Will this proposal disallow this?

    var [
    // a,
    // b
    ] = foo()

    ... in the same way? Or what about?

    var [ , , ] = foo()

    ...if you have a stateful iterator on an array arr, you may want to do:

    var [ , , ] = arr;
    
    // later
    var [x, y] = arr;

    ...where you're expecting the [ , , ] to basically advance the stateful iterator by two steps, such that x and y are assigned from positions 2 and 3, respectively.

It sounds nice to assume that { }, [ ] and [ , , ] destructuring patterns are mistakes by developers, but that's just not a safe assumption.

Since @nzakas has already landed a linter rule for eslint for catching these things, I think that is the better path. Let tooling catch/warn about these, but don't make the language prevent these potentially not-mistake cases.

@rossberg
Copy link
Member

I disagree with this proposal. It creates an irregularity and makes life harder for code generators, but also for people who want to comment out parts of a pattern. I don't think there is any real win either. Moreover, as Brian pointed out, destructuring without a RHS is an error already.

@nzakas
Copy link

nzakas commented Oct 22, 2015

I think [] = [] should definitely be an error.

Another scary pattern is this:

let { loc: [] } = foo;

@rossberg-chromium this isn't just about variable declarations, there are also assignments. You'd still be able to comment out parts of a pattern, just not every part at once. You're also missing this use case completely:

let { foo: {} } = bar;

@getify I'm having a hard time understanding why commenting out every property at once is useful for debugging. Isn't this:

var {
// x,
// y
} = foo();

The same as:

// var {
// x,
// y
// } = foo();

?

@caitp
Copy link
Contributor

caitp commented Oct 22, 2015

Isn't this:

var {
// x,
// y
} = foo();

The same as:

// var {
// x,
// y
// } = foo();

?

The RHS is evaluated before the binding pattern is walked, so foo() is still invoked, regardless of whether any bindings are created

@nzakas
Copy link

nzakas commented Oct 22, 2015

@caitp yes, but unless you're relying on side effects, that difference is inconsequential.

@nzakas
Copy link

nzakas commented Oct 22, 2015

Assuming goes both ways. :)

In any event, inability to comment out code just the way you want it doesn't seem like it should be a blocker for this. We all have editors, most have shortcut keys for inserting comments. If you need to use two keystrokes instead of one to help prevent unnecessary errors that are hard to find, that seems like a good tradeoff (IMHO). You can always move the RHS of an assignment to the next line temporarily and get the same effect.

@getify
Copy link
Contributor

getify commented Oct 22, 2015

intentional side effects sometimes are as important but yet benign as console.log statements inside foo(), again for debugging purposes.

@jeffmo
Copy link
Member

jeffmo commented Oct 22, 2015

There's always...

/*var {
// x,
y
} =*/ foo();

Clarifying edit: I don't feel strongly about this, but I also don't think that changes to the ability to single-line-comment at dev time for some rare set of cases is all that compelling a reason not to do this. I mean if this is really an issue, what do you do when some code does var {x} = foo();? Or even just var x = foo();?

@getify
Copy link
Contributor

getify commented Oct 22, 2015

you can always move the

yes of course you can. but my point is I shouldn't have to.

you make the one-sided assumption that my case ought to require me to type/edit more. i'll reiterate my similarly one-sided assumption/assertion from earlier: if you want protection from this "mistake" in your code, you should have to use a smart tool to protect you. but don't force that assumption on the rest of us.

@rwaldron
Copy link
Contributor

FWIW, I agree with @getify and @rossberg-chromium for the same reasons they've stated. Please don't take this as dog-piling, as I will gladly argue against this at the next meeting ;)

@bterlson
Copy link
Member

Can someone give a little more detail about how removing a binding is useful for debugging purposes? It seems unlikely that removing a binding would have an interesting effect on a program.

Also, it's clear this needs consensus!

@bterlson bterlson added the needs consensus This needs committee consensus before it can be eligible to be merged. label Oct 22, 2015
@rwaldron
Copy link
Contributor

@bterlson, less about the specific thing you might do and more about the strange error you'll encounter when doing perfectly reasonable things during development/debugging. In fact, you might want to eliminate all bindings to observe the outcome—it doesn't really matter why, it's just something a developer might do, and I think the language doesn't need to prohibit it just because it can. This is the sort of thing that dev tools (i.e. linters) can handle—if the end developer wants to.

@getify
Copy link
Contributor

getify commented Oct 22, 2015

in one of my cases, the properties I was destructuring were getters. the side effects were noisy console.log statements that I wanted to avoid by simply commenting out the assignment item in the pattern.

@ljharb
Copy link
Member

ljharb commented Oct 22, 2015

When commenting out every destructuring property in an object destructuring, could you not just add a nonexistent garbage property, and still do your debugging without having to worry about whether "no destructuring patterns" are an error?

I think the real question here is, which is going to be more common: that an empty-bindings destructuring pattern is a developer mistake (worth noting that things like the TDZ attempt to address developer mistakes, and I think this is indeed the language's job), or, that an empty-bindings destructuring pattern is a temporary thing that an educated developer is doing for debugging?

I tend to believe that both the former case is more common, and that in the latter case, the developer will not have a practical problem avoiding this error during debugging.

@rwaldron @getify @rossberg-chromium, do you really think both that the former (a newbie developer making a mistake) is far less common than a developer needing to debug destructuring with things that have side effects such that they need to comment out every destructuring binding, and that discontinuing to allow the latter would be excessively burdensome on an experienced developer during their debugging?

@bterlson
Copy link
Member

@rwaldron I'm not arguing this should be prohibited just because it's possible to prohibit. I see it as a mistake people might appreciate an error for like omitting other BindingIdentifiers. The error seems especially hard to spot in nested forms, and with initializers it gets harder still. But if this isn't a valuable error then I agree no reason to throw it.

@getify
Copy link
Contributor

getify commented Oct 22, 2015

@bterlson

if this isn't a valuable error then I agree no reason to throw it.

that's not really the question, IMO. is it useful? it seems likely it might be useful to some people in some cases. but it's also something that clearly it's something that linting tools can easily check for, giving developers an opt-in for that checking, rather than locking developers into it by language design.

I think adding a lock-out to the language design deserves a higher bar than "useful/valuable error to some" (or even "many").

@getify
Copy link
Contributor

getify commented Oct 22, 2015

@ljharb i don't think it's fruitful to have a guessing-match at who's got the corner on more common. it's clear what you and nicholas think, but i don't agree. shrugs.

however, no one seems to be addressing my second use-case above... the [ , , ] array-destructuring used to advance the cursor of a persistently-stateful generator. I suppose you'll also claim "very few will ever do that".

so i guess my only counter-argument is that this proposal has only presented one kind of use-case where the mistake happens (nested destructuring where they typed : instead of =). and i've presented two counter-arguments from what it unduly restricts.

@bterlson
Copy link
Member

You can use linting tools to find errors, but I believe some errors are helpful enough to throw anyway. Regardless, I am not strongly in favor of making this an error (we allow many empty braces in various places already). I can see the rationale for keeping the current semantics.

@anba
Copy link
Contributor

anba commented Oct 22, 2015

Similar to @getify's [ , , ] example for destructuring assignment, I've also already used [...[]] = iterable to drain an iterable. IMO a quite handy shortcut when working on the REPL compared to the more verbose for (let ignored of iterable) ;.

@rossberg
Copy link
Member

@ljharb:

When commenting out every destructuring property in an object
destructuring, could you not just add a nonexistent garbage property, and
still do your debugging without having to worry about whether "no
destructuring patterns" are an error?

Sure, but why require such extra tedium?

I think the real question here is, which is going to be more common: that
an empty-bindings destructuring pattern is a developer mistake (worth
noting that things like the TDZ attempt to address developer mistakes, and
I think this is indeed the language's job), or, that an empty-bindings
destructuring pattern is a temporary thing that an educated developer is
doing for debugging?

Aside: TDZ is not just to prevent mistake, but to have meaningful behaviour
for something like const declarations, and to be future-proof regarding
other possible extensions to declarations (e.g. guards were under
discussion at the time).

In most other places, JavaScript consciously does not help the programmer
avoiding mistakes, quite the opposite in fact.
Special case rules and irregularities are generally bad -- principle of
least surprise, refactoring, compositionality, etc. Before introducing
another such case I'd like to see strong evidence that it actually
addresses a real problem, i.e., that the supposed mistake is a common (and
difficult to diagnose) error in practice. I find that very hard to believe.

@jeffmo
Copy link
Member

jeffmo commented Oct 22, 2015

So yea, the fact that empty destructuring itself can incur side effects (i.e. draining an iterator) now seems to me like a compelling reason to not do this.

@bterlson
Copy link
Member

I somewhat contest that the assignment case should allow no identifiers if the binding patterns don't. It's certainly not clear that this is the "proper" way to do it. Wouldn't the same arguments would apply to making the empty assignment forms an error as well. [...[]] = iter is a pretty neat pattern, though not often useful (and perhaps could be solved in other ways, eg. by allowing bindingless rest ala [...] = iter, or by saying forms that pull from iterators like rest and patterns with elisions are allowed to omit bindings).

@mohsen1
Copy link

mohsen1 commented Oct 22, 2015

What if I'm only triggering getters by let { bar: {} } = foo; for some reason?

'use strict';

let foo = {
    get bar() {
        console.log('...');
    }
}

let { bar: {} } = foo;

I agree that's a horrible code, just saying it can be valid...

@allenwb
Copy link
Member Author

allenwb commented Oct 23, 2015

@mohsen1

What if I'm only triggering getters by let { bar: {} } = foo; for some reason?

Loose the let (ecause you aren't declaring any variables) and just say:

{ bar: {} } = foo;

@nzakas
Copy link

nzakas commented Oct 23, 2015

I'm not sure I understand the distinction between declarations and assignments, the error case is the same in both. In the former, the intent is to create new bindings but you fail to do so, in the latter, the intent is to assign a value and you fail to do so.

And what about destructured function arguments? It seems like you can be bitten by this in numerous places, and I'm not sure I can rationalize why you'd get a safety net for one place but not others.

@getify
Copy link
Contributor

getify commented Oct 23, 2015

@allenwb of course, that doesn't just work as cited, because then you'll have to remember to put the ( ) around the whole assignment...

...which is not only unnecessary but also invalid when the 'let' is present.

@allenwb
Copy link
Member Author

allenwb commented Oct 23, 2015

@getify oops, of course.
since it is just being evaluate for effect, maybe:

0,{ bar: {} } = foo;

@allenwb
Copy link
Member Author

allenwb commented Oct 23, 2015

@nzakas

I'm not sure I understand the distinction between declarations and assignments,

The big difference I see is that declarations have non-local effect upon the static meaning or validity of parts of a program. For example, a declaration can shadow a declaration that exists in an outer scope and reference in an inner scope. A declaration can also make other declaration invalid (duplicate names). An assignment pattern does not have such non-local static effects.

In my proposal at the top of this thread, I included a non-bindings check for destructuring function parameters.

@rossberg
Copy link
Member

@allenwb, you can interpret the argument that some of us have made as: this change is potentially detrimental even to your criterion 1 ("All other things being equal, language should help humans write correct code."). I mentioned refactoring and principle of least surprise as reasons.

@nzakas
Copy link

nzakas commented Oct 23, 2015

@allenwb of your guiding principle is writing correct code, the local vs. nonlocal effects should be secondary to the fact that the assignment didn't actually assign, shouldn't it?

@rossberg-chromium you're arguing that principle of least surprise says { bar: {} } = foo; not doing an assignment is not surprising?

@rossberg
Copy link
Member

@nzakas, no, I don't see what's surprising about that (except for somebody who doesn't know destructuring). Destructuring binds 0..N variables, or assigns 0..N mutables. A needless discontinuity between 0 and 1..N is surprising (and annoying).

@ljharb
Copy link
Member

ljharb commented Oct 23, 2015

I feel like many developers see destructuring assignment as simply sugar for assigning to variables - var/let/const only ever creates 1 binding, so 1..N for destructuring assignment seems like a very logical step. From my perspective, allowing zero bindings is the discontinuity.

@bterlson
Copy link
Member

I too am curious if @rossberg-chromium would argue for making the binding identifier of let decls optional. It would allow binding 0..N variables and let you comment out a binding without commenting out its side-effecting initializer. Can leave it to linters to give you an error when you type let; if you want.

@michaelficarra
Copy link
Member

@bterlson That's already a valid program.

@bterlson
Copy link
Member

Riiight, although you'd likely get an error if let isn't declared. Still curious though - is that ambiguity the only reason the binding identifier isn't optional?

@michaelficarra
Copy link
Member

If not for that ambiguity, I would be in favour of that proposal.

@mohsen1
Copy link

mohsen1 commented Oct 23, 2015

@allenwb

Loose the let

I'm convinced. If I'm not declaring anything I should be warned that let is not doing anything there...

@getify
Copy link
Contributor

getify commented Oct 23, 2015

I dunno if this is a guiding principle of JS's design or not, but I believe it should be: any desired feature (or in this case, error) should be implemented in the least-burdensome way to those it would adversely affect -- adverse effects should be minimized.

The proposal(s) here seek to shut out valid cases of usage by telling affected developers "just do something different". however, the main concern of this thread (mistake avoidance) could be handled in a less burdensome way by letting tools handle the checks.

It's basically zero-cost to use tools to warn on this mistake for developers that want it, since such support has already landed. By contrast, it's a non-zero cost to shift the burden to developers to find some other way of doing any of the use-cases mentioned earlier.

@bterlson
Copy link
Member

@getify obviously adverse effects and burdens on developers should be minimized. No one disagrees with that.

@nzakas
Copy link

nzakas commented Oct 25, 2015

@getify the difference is that you're claiming error avoidance is the job of tools, this proposal says error avoidance is the job of the language. I'm also in the latter camp, as you cannot expect everyone to be using tools to flag all errors nor can you assume all tools designed to find errors will find all bad patterns.

@getify
Copy link
Contributor

getify commented Oct 25, 2015

@nzakas I am not broadly decrying all error avoidance in language design. There's an awful lot of good errors that JS asserts.

This particular error avoidance doesn't just avoid errors (though you clearly see it only as errors). In a place where there's a tension between error avoidance and valid uses, I think that's a space where an argument can be made that tools can and should fill the gap, since tools are opt-in and configurable; language design is (largely) not.

@mandel59
Copy link

Empty binding has no problem in semantics but just problematic when human write JS. How do you think about JS code generated by programs? If empty binding is not allowed, I would have to make more effort to eliminate empty binding. Empty code and dead code may be problematic, but I'd like empty or dead code detection to be optional rather than required.

@allenwb
Copy link
Member Author

allenwb commented Oct 30, 2015

@mandel59 see my comment above

@benjamn
Copy link
Member

benjamn commented Nov 17, 2015

Though I agree with @getify and others that enforcing this proposal feels like the job of a linter and not the language itself, I'm worried that the current proposal will miss a large class of realistic mistakes, regardless of whether it is implemented by a linter or by the language.

The most common mistake of this kind that I've encountered in real code is not "declarations that bind nothing," but confusion over the proper syntax for default values.

A programmer who doesn't yet understand every detail of destructuring syntax could reasonably think that this code extracts the x and y properties from the result of f() and provides a default value of {} for the y property:

let { x, y: {} } = f();

Of course what the programmer really wants is

let { x, y = {} } = f();

and there is real value in warning about the first pattern.

However, if I understand this proposal, because the declaration at least binds x, there would be no warning for code like this. But it still feels like a mistake (in the spirit of this proposal) that the y sub-pattern doesn't bind anything.

@getify
Copy link
Contributor

getify commented Nov 17, 2015

As long as we're discussing potential mistakes that could potentially be caught "early" by tooling (or... ugh... the language), I personally think these are other common mistakes around destructuring:

let { x } = ..
let [ y ] = ..

Why? Because object destructuring and array destructuring don't have a guard built into them -- which I think they should have had! -- to handle when the r-value cannot be destructured.

There's some cases that could be spotted easily, like:

let { x } = null;
let [ y ] = undefined;

But obviously the r-value is not always statically knowable, like let { x } = foo(). There's no plausible syntactic solution I'm aware of to this particular form of the problem.

But in the nested destructuring case, there is. Consider:

let { x: { y } } = ..
let [ [ z ] ] = ..

In these cases, if in their respective r-values, x (or 0th element in the array) are not present, or are present but null or undefined, we know the destructuring assignments here will fail. Solution?

let { x: { y } = {} } = ..
let [ [ z ] = [] ] = ..

The default value of {} or [] prevents such errors. So, if we're talking about mistakes that can happen that we could have tooling/language-mechanics prevent, maybe we should consider enforcing that a nested object/array destructuring assignment must always have a declared default value (whatever it is, even the empty ones as shown).

@benjamn
Copy link
Member

benjamn commented Nov 17, 2015

@getify Though you've phrased that as an additional proposal, I get the feeling what you really mean is that we shouldn't get started down the path of adding warnings to the language as we think of them, because there are lots of corner cases that might potentially indicate programming mistakes, and we don't have a good theoretical framework for capturing all of them a priori. It's fine for linters to accumulate rules in response to common errors programmers actually make, but it's inappropriate for the language to enforce an incomplete set of noisy warnings.

To make this argument more concrete, imagine specifying an error message for @getify's strawman rule about default values being required for nested objects/arrays. Can you really hope to word such a nuanced warning in a way that non-expert programmers can easily understand it? If not, the supposed motivation of helping the programmer avoid mistakes is greatly diminished. A linter, on the other hand, could link to a wiki page on its project website for further explanation, and it's acceptable for a linter to be wrong sometimes, because you opted into using it. It can even read a configuration file to determine what rules you care about enforcing. The language specification itself has none of these luxuries.

@getify
Copy link
Contributor

getify commented Nov 18, 2015

@benjamn -- +1

@bterlson
Copy link
Member

Closing this as there was not consensus for changing the existing semantics. Thanks everyone for all the input!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs consensus This needs committee consensus before it can be eligible to be merged. normative change Affects behavior required to correctly evaluate some ECMAScript source text spec bug
Projects
None yet
Development

No branches or pull requests