Overly complicated Array.from?
On Wed, Dec 25, 2013 at 7:33 PM, David Bruant <bruant.d at gmail.com> wrote:
I was reading the current spec for
Array.from
and it felt too complicated to me.
I've been following the specification of Array.from
very closely since the
day Dave and I first designed it and it's exactly as complicated as it
needs to be for what it needs to be able to do.
I think that if all of these objects had a good default
@@iterable
, there wouldn't be a need for the array-like part ofArray.from
. The "good default" most likely being based on.length
, etc.
The array-like part is for all of those objects that won't have an
@@iterator
, for one reason or another, and for useful shimming in ES5
runtimes. Array.from
can be implemented today for array-likes without
Symbols + @@iterator
and the same code using Array.from
will work correctly
with the built-in Array.from
once those if (typeof Array.from === "undefined") {...
check evaluate to false
.
Le 26/12/2013 05:00, Rick Waldron a écrit :
The array-like part is for all of those objects that won't have an
@@iterator
, for one reason or another
I must have missed these reasons. No @@iterator
also means these objects
cannot be iterated with via for-of loops by default and I can't think of
a good reason for that for any of the listed (arguments
, NodeList
,
arrays from different windows, typed arrays).
Do you have a link to previous discussions on this topic or a summary if
that can be explained quickly?
and for useful shimming in ES5 runtimes.
Even if Array.from
relies on @@iterator
for runtimes with symbols, it
doesn't prevent runtimes without symbols to embed the iterator logic in
the Array.from
source (which is exactly what your prolyfill does).
Found the relevant part of [the notes][1]:
BE: Let's not remain slaves to legacy, Array.from, for-of and spread use only iterable.
RW: What about pre ES6 environment?
BE: Can fall back to array-like if needs.
I guess this is where I differ as I don't see a need. In ES5
environments, the default @@iterator
can be embedded in Array.from
,
leading to something like (worst case for explanatory purposes):
Array.from = function(x){
if(/*x is a NodeList*/){
// polyfill default NodeList[@@iterator] behavior to create the array to return
}
if(/*x is an Arguments*/){
// polyfill default Arguments[@@iterator] behavior to create the array to return
}
// ...
}
```
Most likely all of these `@@iterator` polyfills are the same array-like
traversals, so there shouldn't be a need to separate each case, they
most likely all use the same logic.
Rick Waldron:
> The array-like part is for all of those objects that _won't_ have an
> `@@iterator`, for one reason or another
The Conclusion/Resolution section of [the notes][1] suggests: "Add iterator protocol to arguments object (should exist on all things."
I went quickly through all the meeting notes I could find and didn't
find something about some objects not having an `@@iterator`.
[1]: https://github.com/rwaldron/tc39-notes/blob/master/es6/2012-11/nov-29.md#revisit
There is still the issue of potential libraries that produce arraylikes that don't inherit from a built-in arraylike prototype: those won't benefit from your polyfill without changing their inheritance strategy. (I don't know whether it's a common issue.)
Le 27/12/2013 19:10, Claude Pache a écrit :
There is still the issue of potential libraries that produce arraylikes that don't inherit from a built-in arraylike prototype: those won't benefit from your polyfill without changing their inheritance strategy.
I don't understand the expression "inherit from a built-in arraylike prototype". Could you explain this further?
(I don't know whether it's a common issue.)
I think any use case involving libraries can be solved. In an ES6 world, a library would make Array.from work via setting an appropriate @@iterator on the objects it generates. Based on what I suggested (internalize iterators in Array.from code for polyfills), the ES5 equivalent is to override Array.from as such:
(function(){
var nativeArrayFrom = Array.from;
Array.from = function(x){
if(/*x is of my library type*/){
/* generate an equivalent array using the traversal logic
that would be used for its @@iterator in ES6
*/
}
else{
return nativeArrayFrom(x);
}
}
})()
Granted, it's not super elegant solution, but it does work. The overhead becomes significant only in the degenerate cases where dozens of libraries override Array.from.
David Bruant wrote:
Based on what I suggested (internalize iterators in Array.from code for polyfills), the ES5 equivalent is to override Array.from as such:
(function(){ var nativeArrayFrom = Array.from; Array.from = function(x){ if(/*x is of my library type*/){ /* generate an equivalent array using the traversal logic that would be used for its @@iterator in ES6 */ } else{ return nativeArrayFrom(x); } } })()
This seems overcomplicated. Isn't the likelier code something like
Array.from || (Array.from = function(b) { var a=[]; for (var i=0; i<b.length; i++) a.push(b[i]); return a; });
Isn't the whole point to impute arraylikeness to the parameter?
Granted, it's not super elegant solution, but it does work. The overhead becomes significant only in the degenerate cases where dozens of libraries override Array.from.
David, I took your side in the TC39 meeting, as the meeting notes disclosed. Rick prevailed (I think, my memory is hazy). You want the polyfillers to pay the price, while Rick proposes that ES6's built-in absorb arraylike fallback handling.
The difference is not in the polyfill (old browser) case, but in the present and future (ES6 and above) cases: some objects will remain arraylike yet lack @@iterator. Why shouldn't Array.from help them out?
Le 28 déc. 2013 à 13:05, David Bruant <bruant.d at gmail.com> a écrit :
Le 27/12/2013 19:10, Claude Pache a écrit :
There is still the issue of potential libraries that produce arraylikes that don't inherit from a built-in arraylike prototype: those won't benefit from your polyfill without changing their inheritance strategy.
I don't understand the expression "inherit from a built-in arraylike prototype". Could you explain this further?
Examples of what I tried to designate by the compact expression "built-in arraylike prototype": NodeList.prototype
, Array.prototype
, Arguments.prototype
. For instance, if an object inherits from NodeList.prototype
, there is a good chance that it will be detected as NodeList by your polyfill, and therefore be handled as an array-like. (Otherwise, yes, monkey-patching Array.from
case-by-case remains possible.)
Le 28/12/2013 15:25, Brendan Eich a écrit :
This seems overcomplicated. Isn't the likelier code something like
Array.from || (Array.from = function(b) { var a=[]; for (var i=0; i<b.length; i++) a.push(b[i]); return a; });
Isn't the whole point to impute arraylikeness to the parameter?
In any case the important point is that it's possible to implement in an
ES5 env whatever behavior is expected from Array.from
in an ES6 env.
David, I took your side in the TC39 meeting, as the meeting notes disclosed. Rick prevailed (I think, my memory is hazy).
It's what I read from the notes too, but I feel something may have been overlooked.
You want the polyfillers to pay the price, while Rick proposes that ES6's built-in absorb arraylike fallback handling.
The difference is not in the polyfill (old browser) case, but in the present and future (ES6 and above) cases: some objects will remain arraylike yet lack @@iterator.
In ES6 and above, why would one create such an object? What's a good use
case?
My understanding of the current consensus is that an arraylike without
@@iterator wouldn't work for for-of loops nor spread. Why not just
create an array? jQuery and Zepto want to subclass Array
(one creates
arraylike, the other does subclass setting __proto__
). It wasn't
possible in ES5, but is in ES6 with classes (and the super+@@create
infrastructure).
I feel that all the cases that justified arraylikes in the past have much better alternatives in ES6. My little experience building a Firefox addon even suggests that sets replace arrays in most situations as most of what I do with arrays is .push, for-of and .map/filter/reduce (by the way, Set.prototype needs these too, but another topic for another time).
Why shouldn't Array.from help them out?
If these objects have a good reason to exist in an ES6 and above world, I agree, that's a good point. But is there a use case justifying their existence?
On Sat, Dec 28, 2013 at 11:37 AM, David Bruant <bruant.d at gmail.com> wrote:
In ES6 and above, why would one create such an object? What's a good use case? My understanding of the current consensus is that an arraylike without @@iterator wouldn't work for for-of loops nor spread. Why not just create an array? jQuery and Zepto want to subclass
Array
(one creates arraylike, the other does subclass setting__proto__
). It wasn't possible in ES5, but is in ES6 with classes (and the super+@@create infrastructure).
jQuery is a bad example to use in this case, because it will never include features that can't be made to work consistently across all platforms that are supported. This includes jQuery 2.x, which has no less and no more features or capabilities than jQuery 1.x (and never will).
I feel that all the cases that justified arraylikes in the past have much better alternatives in ES6. My little experience building a Firefox addon even suggests that sets replace arrays in most situations as most of what I do with arrays is .push, for-of and .map/filter/reduce (by the way, Set.prototype needs these too, but another topic for another time).
Realistically, this is a minority use case. The most common use case is a web-based application that often must work on platforms as old as IE8, and that shouldn't limit the progress and evolution in ES6 features.
If these objects have a good reason to exist in an ES6 and above world, I agree, that's a good point. But is there a use case justifying their existence?
In the ES6 world, there will be userland and platform objects that can't be upgraded to real iterables, eg. application code that must behave correctly on platforms with and without @@iterator (ie. browsers that have large market share but don't auto-update). Array.from can be shimmed such that code like the following will work correctly on both older ES3-5 (Array.prototype methods shimmed for ES3) or ES6 platforms:
if (typeof Array.from === "undefined") {
...shim it.
}
function f() {
// Works correctly on browsers that have implemented @@iterator on arguments objects
// as well as those that have not.
return Array.from(arguments).map(...);
}
// Or...
// Works correctly on browsers that have implemented @@iterator on NodeList objects
// as well as those that have not.
Array.from(qSA(".selector")).forEach(...);
for-of
won't even exist on these older platforms, but for code that only
needs to run on ES6 platforms, Array.from
provides a "spread" or "make
iterable" mechanism for those objects that can't be upgraded (for any
reason, as noted above):
// Turn a jQuery object into an iterable:
Array.from(jQuery(".selector"));
(This can't be done with spread)
Array.from bridges the gap between ES3/5 and ES6 while remaining useful in an ES6 and post ES6 world by providing an "assimilation" path for objects that have pre-existing constraints.
Why can't jQuery do
if (typeof Symbol !== "undefined" && Symbol.iterator) {
jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
}
Le 28/12/2013 20:24, Rick Waldron a écrit :
Realistically, this is a minority use case.
Agreed. I was just trying to share what I felt the future taste like.
The most common use case is a web-based application that often must work on platforms as old as IE8, and that shouldn't limit the progress and evolution in ES6 features.
Ok, I think I better understand what you meant by "can't be iterable".
In the ES6 world, there will be userland and platform objects that can't be upgraded to real iterables, eg. application code that must behave correctly on platforms with and without @@iterator
Ok for userland objects, but do you have examples for platform objects? I feel that it's up to the W3C folks to add appropriate @@iterators to the objects they define... and it's already the case with indexed getters for instance 1. (Firefox has already implemented it, you can for-of the result of qSA \o/). I wonder what you mean by "behave correctly on platforms with and without @@iterator". If you're targetting both types of platforms, you can't use the new facilities (for-of and spread), so you don't need for your objects to adhere to the iterable protocol.
(ie. browsers that have large market share but don't auto-update).
I already see IE10 market share being higher than IE9. IE8 might be the last browser without symbols
for-of
won't even exist on these older platforms, but for code that only needs to run on ES6 platforms, Array.from provides a "spread" or "make iterable" mechanism for those objects that can't be upgraded (for any reason, as noted above):
// Turn a jQuery object into an iterable: Array.from(jQuery(".selector"));
(This can't be done with spread)
Why can't the code that only run in ES6 provide its own "make iterable" facility? As you said, it's polyfillable. I think spread is even "compile-to-ES3"-able.
Array.from bridges the gap between ES3/5 and ES6 while remaining useful in an ES6 and post ES6 world by providing an "assimilation" path for objects that have pre-existing constraints.
What you're indirectly saying here is that Array.from is not useful in an ES6-only code base (since @@iterator+spread can be used). Libraries (Array._from?) and tooling (compile spread to ES3) can be enough for the transition, no?
I believe that Array.from's only purpose is to provide guidance for polyfills for people to use in ES3/ES5 code; nobody writing ES6 would ever use it. In essence it's saying "TC39 likes es6-shim more than Underscore, and is helping tell them what should be in it, with the hope that people use it." At least, that's what it seems to me.
On Saturday, December 28, 2013, Domenic Denicola wrote:
Why can't jQuery do
if (typeof Symbol !== "undefined" && Symbol.iterator) { jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; }
For the same reason I've stated here any time anyone ever brings up anything anything jQuery "might" do: jQuery doesn't ship anything in core that cannot be made to work consistently in all browsers that both 1.x and 2.x are expected to support. Additionally, jQuery has never and will never ship "conditional" features in the core library.
On Sat, Dec 28, 2013 at 5:44 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:
I believe that Array.from's only purpose is to provide guidance for polyfills for people to use in ES3/ES5 code; nobody writing ES6 would ever use it.
Ignoring any of the previous benefits I've discussed, it seems you're forgetting about the map function feature of Array.from?
Yes, I am; many apologies. I also forgot about how it would be useful for subclasses, e.g. Elements.from(nodeList), since subclasses don't have their own dedicated spread syntax. Withdrawn in full.
Le 29/12/2013 00:11, Rick Waldron a écrit :
Ignoring any of the previous benefits I've discussed, it seems you're forgetting about the map function feature of Array.from?
Ok, so to re-focus for those following at home, there are 3 cases to consider for authors:
- code only aiming at ES3/5
- code aiming at both ES3/5 and ES6 environments
- code only aiming at ES6 envs.
For 1), let's all keep doing what we've been doing. In ES3/5, there is
not really a notion of iterable protocol as it's not used by the
language as it is in ES6.
For 3), from an iterable to an array, it takes [...iterable]
IIUC,
no need for Array.from at all.
2) is the subtle case. There is only one code base. Because it needs to
work in ES3/5, it can't use @@iterable (like jQuery as Rick stated).
However, ES6 code may want to iterate over the arraylikes generated by
the library with for-of and spread. This is where Array.from comes handy
if it works both for @@iterables and arraylikes.
But an Array._from library could work equally well for this purpose. No
need for the built-in Array.from to handle arraylikes.
Dominic Denicola:
I also forgot about how it would be useful for subclasses, e.g. Elements.from(nodeList), since subclasses don't have their own dedicated spread syntax. Withdrawn in full.
Oh... super true. Array.from or perhaps less confusingly "just-from" enables to convert any iterable to another iterable (assuming proper @@create setup). But that's an ES6-only use case and is unrelated to the arraylike handling I think. Side note: it's somewhat ironic that Array carries 'from' given it's the only "class" that doesn't need it per case study for 3) above :-)
David Bruant wrote:
it's somewhat ironic that Array carries 'from' given it's the only "class" that doesn't need it per case study for 3) above :-)
But Array is the return type.
It's always the return type of Array.from(x)
, but not the return type of
Array.from.call(Whatever, x)
. I called Array.from
"just-from" in my
previous message as an attempt to reduce the confusion.
In the latest draft, step 1 of Array.from
is "Let C be the this value".
There are different return points that all return 'A' and 'A' created at
step 8.a.i as the result of C.[[Construct]]. And C.[[Construct]] doesn't
have to return an Array.
That's at least my understanding of the current draft.
"just-from" is a function that turns the iterable passed as argument to an array-like. It'll be an Array for Array.from but whetever else for Whatever.from.
David Bruant wrote:
It's always the return type of
Array.from(x)
, but not the return type ofArray.from.call(Whatever, x)
.
Of course, but why is this a problem for the name? Collection.from
for
Collection extends Array
carries the same connotation.
Let's stick to real problems! The name is not a problem, AFAICT.
Le 29/12/2013 14:42, Brendan Eich a écrit :
Of course, but why is this a problem for the name?
Collection.from
forCollection extends Array
carries the same connotation.Let's stick to real problems! The name is not a problem, AFAICT.
The part you answered to was a sidenote in my original message :-/ I never meant to question the name.
Back to the real problem. As a summary, Rick explained that the problem to solve was ES6 code using ES3/5 codebases and the need to easily turn the arraylikes the latter define to real iterable for use in the former. It is an author's problem today. By that I mean that it's not a language expressivity problem (like the one WeakMap, Proxies or Symbols solve) and it is a temporary problem (granted, the transition period may last 10 years, but it's still temporary).
I believe this problem should be solved by authors via libraries and/or tooling and that the language should not carry a scar of a transitional problem. As a matter of fact, the library to solve the transitional problem is 20 (!) straightforward lines of code and already exists 1 (and with Rick's blessing, it'll be open source), so I don't believe assistance from the language is needed.
The costs of supporting arraylikes aren't big (zero in runtime?) though. Eventually, this part will just be dead code. Not optimal, but not a big deal.
I was reading the current spec for Array.from and it felt too complicated to me. Currently, at a high-level it reads like:
@@iterable
symbol), create a fresh array made of the values iterated on with the iteratorlen = [[Get]] ('length')
and from0
tolen-1
, copy the values of the array-like in a fresh array to be returned.Note that between the two parts, a good share of spec is duplicated.
For the rationale, the wiki at strawman:array_extras states:
I think that if all of these objects had a good default
@@iterable
, there wouldn't be a need for the array-like part ofArray.from
. The "good default" most likely being based on.length
, etc.