fail-fast object destructuring (don't add more slop to sloppy mode)

Brendan Eich brendan at mozilla.com
Tue Jan 1 23:06:42 PST 2013


Andreas Rossberg advocated "fail-fast object destructuring", and Dave 
Herman started a thread last June at

https://mail.mozilla.org/pipermail/es-discuss/2012-June/023710.html

and ending near here:

https://mail.mozilla.org/pipermail/es-discuss/2012-July/024043.html

I'd like to revive this as an open issue for ES6.

With the recent mega-thread on "excluding features from sloppy mode" 
(tail post: 
https://mail.mozilla.org/pipermail/es-discuss/2012-December/027746.html), I 
believe that everyone involved agrees that we should not add more slop 
in ES6, where new slop might then require more strictness in a future 
strict mode.

We can't afford the spec and implementation and user-brainprint costs of 
a never-ending lattice of sloppy and strict combinations. Worse, runtime 
changes in a stricter strict will create the same hazards for 
ES6-stricter-strict vs. ES5 strict that we had with ES5 strict vs. ES3.

So where is new slop in ES6? The obvious place is destructuring, 
proposed for ES4 and prototyped in Opera (array patterns only), and then 
in SpiderMonkey & Rhino. Destructuring as proposed is thin syntax for 
property references on the right of assignment expressions or binding 
initializations:

js> let o = {p: 42}
js> let p = o.p
js> p
42
js> let {p:q} = o
js> q
42

Thus it follows that if o has no property p, the p and q bindings are 
initialized to undefined:

js> let o = {r: 42}
js> let p = o.p
js> p
js> let {p:q} = o
js> q
js>

This is future-hostile to any pattern-matching construct 
(http://wiki.ecmascript.org/doku.php?id=strawman:pattern_matching), we 
want the notation to fail by throwing in the binding and assignment 
cases, so that a match construct can fail over to the next pattern.

Even ignoring the future, this fail-soft approach, while consistent with 
missing property handling in JS, is slop that tends to hide bugs or make 
them hard to fix -- the undefined flows far afield before being 
dereferenced (assume an explicit toString(radix) call on p or q) and 
inevitably throwing.

(Really, I made JS impute undefined for missing properties back in those 
ten days in May 1995 because I had no time to add matching, try-catch, 
existential operator syntax, or any other means for programmers to say 
when they wanted to "object-detect" rather than fail-soft. If I had had 
more time, I'd have done something better. Developers get burned by 
typos too often. When we added try/catch exception handling in ES3, we 
did not revisit this primal sin.)

The other kind of slop to which Andreas objected is the implicit 
ToObject call on the right-hand side of the '=' in a destructuring 
binding with initialiser, or a destructuring assignment expression. 
That's just another implicit conversion, the bane of JS developers' 
existence which strict mode can't banish. Such implicit conversion does 
help code of the form that Allen sketched at 
https://mail.mozilla.org/pipermail/es-discuss/2012-June/023719.html:

let {concat, indexOf, lastIndexOf} = ""; //get some string methods

Of course, this is a hard case and easily rewritten to use new 
String("") or String.prototype or equivalent.

We talked about ? prefixing of identifiers in patterns, vs. ! for 
irrefutable (give me undefined if missing) prefixing, which is simply 
future-hostile to pattern matching. The slop-free and pattern-friendly 
changes from ES4&6 destructuring would thus entail:

1. No ToObject(RHS).
2. Exception on missing property selected without ?-prefix.

We thought a ? suffix was ambiguous, or just confusing, in object 
literals, since the grammar for expressions must cover the grammar for 
patterns, but there was no formal ambiguity. I recall Dave at least 
proposing a ?-suffix at first.

Meanwhile, TypeScript has appeared, and it uses ? after identifiers in 
its structural types (interfaces):

interface Friend {
   name: string;
   favoriteColor?: string;
}

I think the refutable-by-default patterns need a ? modifier only for a 
"leaf" identifier in a pattern, not for non-terminal identiifers. For 
example:

let {p: {q: r}} = deep_o;

wants to get o.p.q and bind it to r.

With the current irrefutable design, if deep_o = {} or another object 
lacking a {q:...} sub-object named p, then this example will fail with a 
TypeError even though a shallow example fails-soft (or fails slowly, 
really: fails later) by pulling undefined out as the value of p and 
binding it to the local (as in the first example in this post).

With a refutable-by-default design, however, it seems to me nonsensical 
to support ? on p as well as q in this example:

let {p?: {q?: r}} = deep_o;

because if deep_o has no property p, what should happen? No binding r at 
all? Or a let binding named r with value undefined? What if there's no ? 
after q in this case -- should that matter?\

We already rejected CoffeeScript's existential operator because of its 
lack of compositionality. See 
https://mail.mozilla.org/pipermail/es-discuss/2012-September/025232.html 
(the section starting "ARB: This is non-compositional"). I think the 
same objection applies to allowing p? in the deep_o case.

I'd appreciate feedback on my thinking here, in case the objection does 
not apply.

In summary, I think the right new-new plan to avoid adding slop (and all 
thanks to Andreas and Dave for keeping this torch aloft) is to revise 
ES6 destructuring thus:

1. No ToObject(RHS).
2. Exception on missing property selected without a ?-suffix.
3. ?-suffix allowed only on leaf pattern identifiers (short or long-hand).
4: the ? is a separate lexeme from the : in long-hand patterns.

Comments welcome. I'll put this on this month's TC39 meeting agenda.

/be


More information about the es-discuss mailing list