Spread and non objects

# Erik Arvidsson (15 years ago)

We've run into an edge case with the spread operator. What should happen if we try to spread null or undefined?

f(...undefined);

A. Throw an error B. Follow the path of Function.prototype.apply and others and special case null and undefined and just call f with 0 arguments

# John Lenz (15 years ago)

It would seem friendlier for the spread operator to treat it them as a empty array otherwise you end up with code like:

if (x!=null) { f(1,2,...x) } else { f(1,2) }

# Brendan Eich (15 years ago)

On Nov 4, 2010, at 6:07 PM, John Lenz wrote:

It would seem friendlier for the spread operator to treat it them as a empty array otherwise you end up with code like:

if (x!=null) { f(1,2,...x) } else { f(1,2) }

Yeah, null testing is a drag. People skipt it (I hear this about regexp exec -- it should return an empty array on mismatch -- counter-argument is that empty arrays are truthy).

+1 on Erik's plan B.

# Jon Zeppieri (15 years ago)

On Thu, Nov 4, 2010 at 9:12 PM, Brendan Eich <brendan at mozilla.com> wrote:

On Nov 4, 2010, at 6:07 PM, John Lenz wrote:

It would seem friendlier for the spread operator to treat it them as a empty array otherwise you end up with code like:

if (x!=null) { f(1,2,...x) } else { f(1,2) }

Yeah, null testing is a drag. People skipt it (I hear this about regexp exec -- it should return an empty array on mismatch -- counter-argument is that empty arrays are truthy).

The dual of spread-as-operator is spread-as-rest-arg-syntax. What happens in the case of:

function foo(x, y, ...z) { [...] }

foo(1, 2, null);

Will z be bound to an empty array or to an array containing a single null? I'd expect the latter, and that answer seems more in-line with Erik's plan A.

# Jon Zeppieri (15 years ago)

On Thu, Nov 4, 2010 at 10:06 PM, Jon Zeppieri <jaz at bu.edu> wrote:

On Thu, Nov 4, 2010 at 9:12 PM, Brendan Eich <brendan at mozilla.com> wrote:

On Nov 4, 2010, at 6:07 PM, John Lenz wrote:

It would seem friendlier for the spread operator to treat it them as a empty array otherwise you end up with code like:

if (x!=null) { f(1,2,...x) } else { f(1,2) }

Yeah, null testing is a drag. People skipt it (I hear this about regexp exec -- it should return an empty array on mismatch -- counter-argument is that empty arrays are truthy).

The dual of spread-as-operator is spread-as-rest-arg-syntax. What happens in the case of:

function foo(x, y, ...z) { [...] }

foo(1, 2, null);

Will z be bound to an empty array or to an array containing a single null? I'd expect the latter, and that answer seems more in-line with Erik's plan A.

Erm, on second thought, I got that completely wrong. The fact that z would be [null] doesn't pose any problem for plan B. The rest arg syntax always ensures that z is bound to an array, so it makes good sense that the operator would always produce one.

# Jon Zeppieri (15 years ago)

On Thu, Nov 4, 2010 at 10:34 PM, Jon Zeppieri <jaz at bu.edu> wrote:

On Thu, Nov 4, 2010 at 10:06 PM, Jon Zeppieri <jaz at bu.edu> wrote:

On Thu, Nov 4, 2010 at 9:12 PM, Brendan Eich <brendan at mozilla.com> wrote:

On Nov 4, 2010, at 6:07 PM, John Lenz wrote:

It would seem friendlier for the spread operator to treat it them as a empty array otherwise you end up with code like:

if (x!=null) { f(1,2,...x) } else { f(1,2) }

Yeah, null testing is a drag. People skipt it (I hear this about regexp exec -- it should return an empty array on mismatch -- counter-argument is that empty arrays are truthy).

The dual of spread-as-operator is spread-as-rest-arg-syntax. What happens in the case of:

function foo(x, y, ...z) { [...] }

foo(1, 2, null);

Will z be bound to an empty array or to an array containing a single null? I'd expect the latter, and that answer seems more in-line with Erik's plan A.

Erm, on second thought, I got that completely wrong. The fact that z would be [null] doesn't pose any problem for plan B. The rest arg syntax always ensures that z is bound to an array, so it makes good sense that the operator would always produce one.

Oh my.

Take three.

Spread-as-rest-arg-syntax constructs an array. Spread-as-operator splices the elements of an array into an argument list.

So, if a spread operator expression: ...null splices zero values into an argument list, then its counterpart would be something like:

function foo(x, y, ...z) { }

foo(1, 2);

... where we pass zero optional arguments to foo, and z is bound to an empty array. Which is what we'd expect. So, yeah, plan B makes sense.

# Jon Zeppieri (15 years ago)

On Thu, Nov 4, 2010 at 10:48 PM, Jon Zeppieri <jaz at bu.edu> wrote:

Spread-as-rest-arg-syntax constructs an array. Spread-as-operator splices the elements of an array into an argument list.

So, if a spread operator expression: ...null splices zero values into an argument list, then its counterpart would be something like:

function foo(x, y, ...z) { }

foo(1, 2);

... where we pass zero optional arguments to foo, and z is bound to an empty array. Which is what we'd expect. So, yeah, plan B makes sense.

Ugh. Sorry for working out my confusion in posts to this list. Of course, the example above isn't a satisfactory counterpart to the ...null expression, since it doesn't anywhere involve a null. I suppose there is no real counterpart in the rest arg syntax.

# P T Withington (15 years ago)

[2p. from the Lisp world, the putative inventor of the rest/spread operator.]

In Lisp, arglists are lists instead of arrays, the operator is ., and conveniently, the empty list, (), and Lisp null, NIL, are just two ways to write the same atom. In Lisp, option B just falls out:

(f . NIL) === (f . ()) === (f)

+1 for B as the best approximation (in the absence of a List data type).

We can't really have the symmetry that Zeppieri is looking for because you want the rest operator to return an empty array when there are no rest args (rather than null), so you don't need to test for null before manipulating a rest arg.

# Jon Zeppieri (15 years ago)

On Fri, Nov 5, 2010 at 8:14 AM, P T Withington <ptw at pobox.com> wrote:

[2p. from the Lisp world, the putative inventor of the rest/spread operator.]

In Lisp, arglists are lists instead of arrays, the operator is ., and conveniently, the empty list, (), and Lisp null, NIL, are just two ways to write the same atom. In Lisp, option B just falls out:

(f . NIL) === (f . ()) === (f)

+1 for B as the best approximation (in the absence of a List data type).

If the closest analogue of a Lispy list is an ESy array, then I think the empty array would be the best approximation.

We can't really have the symmetry that Zeppieri is looking for because you want the rest operator to return an empty array when there are no rest args (rather than null), so you don't need to test for null before manipulating a rest arg.

I can (finally) articulate my initial intuition (in favor of Erik's plan A). To the extent that the spread operator and rest args should be symmetrical, the following function should be an identity over its domain:

function id(x) { return (function(...x) { return x; })(...x); }

But it can only be an identity when its argument is an array.

Whether or not anyone else cares about this symmetry is another matter.

Second issue: Erik suggests (plan B) that null and undefined are specifically special cased. I can't tell whether Brendan agrees with that or wants spread to be legal on any input whatsoever, because he writes that spread "should return an empty array on mismatch."

# Mike Samuel (15 years ago)

2010/11/5 Jon Zeppieri <jaz at bu.edu>:

On Fri, Nov 5, 2010 at 8:14 AM, P T Withington <ptw at pobox.com> wrote:

[2p. from the Lisp world, the putative inventor of the rest/spread operator.]

In Lisp, arglists are lists instead of arrays, the operator is ., and conveniently, the empty list, (), and Lisp null, NIL, are just two ways to write the same atom.  In Lisp, option B just falls out:

(f . NIL) === (f . ()) === (f)

+1 for B as the best approximation (in the absence of a List data type).

If the closest analogue of a Lispy list is an ESy array, then I think the empty array would be the best approximation.

We can't really have the symmetry that Zeppieri is looking for because you want the rest operator to return an empty array when there are no rest args (rather than null), so you don't need to test for null before manipulating a rest arg.

I can (finally) articulate my initial intuition (in favor of Erik's plan A). To the extent that the spread operator and rest args should be symmetrical, the following function should be an identity over its domain: function id(x) {   return (function(...x) { return x; })(...x); } But it can only be an identity when its argument is an array.

What do you think should happen with id(arguments)? I tend to think it's best to launder arguments where possible to stop the spread of unintended authority early.

And "There’s no obvious reason why ... should not work on any object with a length property and elements limited by length as ES5 Function.prototype.apply does." would suggest f(..."foo") is equivalent to f("f", "o", "o") so there's at least one non-object type that spread might decompose though I don't know how this particular behavior for strings is useful.

# Brendan Eich (15 years ago)

On Nov 5, 2010, at 7:56 AM, Jon Zeppieri wrote:

Second issue: Erik suggests (plan B) that null and undefined are specifically special cased. I can't tell whether Brendan agrees with that or wants spread to be legal on any input whatsoever, because he writes that spread "should return an empty array on mismatch."

No, that was about feedback over the years on regexp.exec's return value!

Back to spread: the general case of ... is to spread an array-like (ES5 hasn't quite defined this, but it is getting there) into positional parameters or positional array initialiser elements. Any non-array-like could be an error (plan A), but for the reason John Lenz cited, ...undefined as a special non-throwing case that is equvalent to [] seems better (plan B).

I dislike the null == undefined taint, useful though it is in real-world JS that does not abstain from == altogether. So I wonder whether ...null should not throw.

Function.prototype.apply is, as Erik's o.p. pointed out, the counter-argument, since (15.3.4.3 step 2 in ES5) it turns null or undefined into [].

What about other primitives? ...42, ...true, and ..."foo" could throw, as Function.prototype.apply would do (15.3.4.3 step 3).

I think matching Function.prototype.apply is best, having reviewed all this. Comments?

# Dean Landolt (15 years ago)

On Fri, Nov 5, 2010 at 11:23 AM, Brendan Eich <brendan at mozilla.com> wrote:

On Nov 5, 2010, at 7:56 AM, Jon Zeppieri wrote:

Second issue: Erik suggests (plan B) that null and undefined are specifically special cased. I can't tell whether Brendan agrees with that or wants spread to be legal on any input whatsoever, because he writes that spread "should return an empty array on mismatch."

No, that was about feedback over the years on regexp.exec's return value!

Back to spread: the general case of ... is to spread an array-like (ES5 hasn't quite defined this, but it is getting there) into positional parameters or positional array initialiser elements. Any non-array-like could be an error (plan A), but for the reason John Lenz cited, ...undefined as a special non-throwing case that is equvalent to [] seems better (plan B).

I dislike the null == undefined taint, useful though it is in real-world JS that does not abstain from == altogether. So I wonder whether ...null should not throw.

Function.prototype.apply is, as Erik's o.p. pointed out, the counter-argument, since (15.3.4.3 step 2 in ES5) it turns null or undefined into [].

What about other primitives? ...42, ...true, and ..."foo" could throw, as Function.prototype.apply would do (15.3.4.3 step 3).

I think matching Function.prototype.apply is best, having reviewed all this. Comments?

Considering the primary use-case for spread is a sugaring for Function.prototype.apply I would expect the same semantics.

# Brendan Eich (15 years ago)

On Nov 5, 2010, at 8:32 AM, Dean Landolt wrote:

Considering the primary use-case for spread is a sugaring for Function.prototype.apply I would expect the same semantics.

"Gimme some sugar, baby!" - Ash Williams

Everyone's into sugar. Good. (Eggs are next. "Eggs have gotten a bad rap, Smithers." - Montgomery Burns)

But really, while spread applied to an array-like (or null or undefined) passed as the only actual parameter of a call to any callable:

callable(...arraylike);

could be thought of as desugaring to the original value of Function.prototype.apply, now that ES5 has fixed the ES3 botch of translating null or undefined thisArg to the global object:

orig_F_p_apply.call(callable, undefined, arraylike);

in general, spread requires more work. We want to allow

callable(...head, middle1, middle2, ...tail);

for example, something Function.prototype.apply can't do. And when the callee expression is

foo.bar(some, args, here, ...rest);

the thisArg should be foo.

Also, spread in array initialisers wins:

const myBigArray = [...yourBigArray, ...plusMore];

Anyway, I agree matching Function.prototype.apply's step 2 special-casing of null and undefined, and step 3 exception for other non-object argArray parameter types, is best. But spread is not just a sugaring for Function.prototype.apply.

# Jon Zeppieri (15 years ago)

On Fri, Nov 5, 2010 at 11:16 AM, Mike Samuel <mikesamuel at gmail.com> wrote:

2010/11/5 Jon Zeppieri <jaz at bu.edu>:

To the extent that the spread operator and rest args should be symmetrical, the following function should be an identity over its domain: function id(x) { return (function(...x) { return x; })(...x); } But it can only be an identity when its argument is an array.

What do you think should happen with id(arguments)? I tend to think it's best to launder arguments where possible to stop the spread of unintended authority early.

No argument here. My claim about id only being an identity when its argument is an array assumes that the rest arg mechanism works as proposed -- i.e., that the rest parameter is always bound to an array, not to an array-like or anything else. So, if symmetry were valuable, then id(arguments) would throw.

But... I hadn't realized that the spread proposal already included array-likes. In that case, there is no symmetry to preserve. The (now badly misnamed) id function will produce an array from an array-like -- and from null or undefined, under plan B. I think that second part feels a bit hack-y, and as Brendan pointed out, spread isn't simply sugar for apply. Plan B is meant to eliminate an inconvenient null check, but I'm a bit concerned that the proposed behavior will simply hide actual bugs, as automatic coercions often do. That said, automatic coercions are commonplace in ES already.

# Erik Arvidsson (15 years ago)

Thanks for all the feedback. I've updated harmony:spread to reflect this.

# Brendan Eich (15 years ago)

On Nov 5, 2010, at 10:30 AM, Jon Zeppieri wrote:

But... I hadn't realized that the spread proposal already included array-likes. In that case, there is no symmetry to preserve. The (now badly misnamed) id function will produce an array from an array-like -- and from null or undefined, under plan B. I think that second part feels a bit hack-y, and as Brendan pointed out, spread isn't simply sugar for apply. Plan B is meant to eliminate an inconvenient null check, but I'm a bit concerned that the proposed behavior will simply hide actual bugs, as automatic coercions often do. That said, automatic coercions are commonplace in ES already.

We really are trying not to add more implicit conversions. Honest!

Nevertheless, the argument based on matching Function.prototype.apply, plus the example John Lenz gave showing how plan A would require a fruitless null-or-undefined test:

if (x != null) { f(1,2,...x) } else { f(1,2) }

sold me on plan B.

# Jason Orendorff (15 years ago)

On Thu, Nov 4, 2010 at 8:07 PM, John Lenz <concavelenz at gmail.com> wrote:

It would seem friendlier for the spread operator to treat it them as a empty array otherwise you end up with code like: if (x!=null) {   f(1,2,...x) } else  {   f(1,2) }

I'm late to the party, but note that you could instead write

f(1, 2, ...x || [])

which doesn't seem like such a burden. And this seems like a rare case to me; the common case will be like

function dispatch(...args) { for (var i = 0; i < this.handlers.length; i++) obj.handle(...args); }

or else

var errors = []; // an array to begin with // lots of code report.push(...errors);

In cases like these, a null or undefined spread-argument would be a mistake. And when that happens, the programmer wants the problem to be detected as soon as possible, which means throwing an exception. The tradeoff here is between having to remember to add "|| []" when it's needed, and the possibility that accepting undefined here would make simple typos pass silently:

// Suppose we want to expand request.arguments into the arguments here. log.write(msgid, ...request.args) // oops, typo, should be request.arguments

// Here the code around the ... is correct, but a caller makes a // mistake that passes silently. function dispatch(obj, method, args) { objmethod; } var myargs = [x, y, z]; dispatch(myobj, mymethod); // oops, forgot 3rd argument, but no error

Of course these things already pass silently, for programmers using Function.prototype.apply, thanks to a special case in the spec. But maybe we shouldn't have more of those. Every special case should have to justify its existence. Function.prototype.apply is a special case; most places that want an array-like object will throw if you pass null or undefined.

# Brendan Eich (15 years ago)

On Nov 5, 2010, at 5:32 PM, Jason Orendorff wrote:

On Thu, Nov 4, 2010 at 8:07 PM, John Lenz <concavelenz at gmail.com> wrote:

It would seem friendlier for the spread operator to treat it them as a empty array otherwise you end up with code like: if (x!=null) { f(1,2,...x) } else { f(1,2) }

I'm late to the party, but note that you could instead write

f(1, 2, ...x || [])

You mean

f(1, 2, ...(x || []))

at least.

which doesn't seem like such a burden. And this seems like a rare case to me; the common case will be like

function dispatch(...args) { for (var i = 0; i < this.handlers.length; i++) obj.handle(...args); }

or else

var errors = []; // an array to begin with // lots of code report.push(...errors);

In cases like these, a null or undefined spread-argument would be a mistake. And when that happens, the programmer wants the problem to be detected as soon as possible, which means throwing an exception. The tradeoff here is between having to remember to add "|| []" when it's needed,

and parentheses.

and the possibility that accepting undefined here would make simple typos pass silently:

// Suppose we want to expand request.arguments into the arguments here. log.write(msgid, ...request.args) // oops, typo, should be request.arguments

// Here the code around the ... is correct, but a caller makes a // mistake that passes silently. function dispatch(obj, method, args) { objmethod; } var myargs = [x, y, z]; dispatch(myobj, mymethod); // oops, forgot 3rd argument, but no error

Is the caller making a mistake or counting on something expected by comparison to Function.prototype.apply, or just by analogy to missing parameters not being errors?

This is mostly a rhetorical question, don't worry about answering. My point is more that users will expect what they expect, not what we think they ought to.

Of course these things already pass silently, for programmers using Function.prototype.apply, thanks to a special case in the spec. But maybe we shouldn't have more of those. Every special case should have to justify its existence. Function.prototype.apply is a special case; most places that want an array-like object will throw if you pass null or undefined.

There's the special case of deviating from the Function.prototype.apply special case. Ok, I get it -- two wrongs don't make a right. Still, this is a close call.

# Erik Arvidsson (15 years ago)

On Fri, Nov 5, 2010 at 17:46, Brendan Eich <brendan at mozilla.com> wrote:

You mean

f(1, 2, ...(x || []))

at least.

No need for the parens here [*]. Do you want it for readability?

[*] harmony:spread#syntax

# Brendan Eich (15 years ago)

On Nov 5, 2010, at 6:19 PM, Erik Arvidsson wrote:

On Fri, Nov 5, 2010 at 17:46, Brendan Eich <brendan at mozilla.com> wrote:

You mean

f(1, 2, ...(x || []))

at least.

No need for the parens here [*]. Do you want it for readability?

Maybe. Or a space. It's unusual to have a low precedence prefix operator that can run right into its operand without any spacing or parentheses.

yield is one example but it must be followed by a space or some kind of left parenthesis or bracket.

[*] harmony:spread#syntax

Thanks for editing this.

The issue remains: EIBTI vs. "consistency" with F.p.apply. I don't think "JS has implicit conversions" as a general rule should weigh in -- we are trying to live that down, so we need a strong, more specific reason.