Why does Array.from also take a mapFn?
On Jun 24, 2013, at 8:42 AM, Jason Orendorff wrote:
According to the January 30 meeting notes, Array.from is getting optional map functionality.[1]
This is motivated by the following example:
class V extends Array { constructor(...args) { super(...args); } } var v, m; v = new V(1, 2, 3); m = v.map(val => val * 2); console.log( m instanceof V ); // false :(
Of course changing Array.from doesn't fix this; v.map() still returns an Array, same as before. And there was already an easy workaround:
m = V.from(v.map(val => val * 2));
The issues (and discussion) is more complex then the above indicates.
The root issue concerns what should Array producing methods such as Map produce when used in an Array subclass. Should the result be an Array instance (as it currently is in ES5) or should it be a new instance of the subclass. If the latter, how is the actual subclass determined. Also experience form other languages suggests that whichever the default is, sometimes you want the other behavior and sometimes an comply different kind of result object is desired.
My recollection is that we first discussed that the existence of Array.from make this issue somewhat less important because, just as you point out, .from can be used in conjunction with anything that produces an Iterable such as V.from(v.map(val => val * 2))
That led to further discision of that usage and we got into things like the last example:
// Turn an array of nodeNames into NodeList of nodes
NodeList.from( ["div"], node => document.createElement(node) );
The option map in Array.from really addressed two issues. First, it allows a mapping to be applied to Iterable that are not Array subclasses and hence do not directly support map. Second, it directly supports the most common use case and avoids the creation of intermediate arrays that are produced if the instance side map method is used:
This workaround seems superior to the proposal, because it's more generally applicable. The same thing works for the results of .filter(), .concat(), etc. Something very similar works for Set:
m = Set(v.map(...));
Clearly not every possible composition of two primitives needs to be a builtin function. Why this particular one?
see above. Arrays and NodeLists were the focus of the discussion.
For reference, this is the thread: esdiscuss/2013-February/028661
From: Allen Wirfs-Brock [allen at wirfs-brock.com]
My recollection is that we first discussed that the existence of Array.from make this issue somewhat less important because, just as you point out, .from can be used in conjunction with anything that produces an Iterable such as V.from(v.map(val => val * 2))
That led to further discision of that usage and we got into things like the last example:
// Turn an array of nodeNames into NodeList of nodes NodeList.from( ["div"], node => document.createElement(node) );
I think I must be missing something. Why is this superior to
NodeList.from(["div"].map(node => document.createElement(node));
which you gave as an example just a few lines above? In other words, why does Array.from
accept a second parameter at all?
Another question that I don't think got an answer, or if it did I was unable to understand it. Why is "map" singled out for Array.from
instead of, say, "filter"? It seems arbitrary.
On 6/24/2013 9:42 AM, Domenic Denicola wrote:
That led to further discision of that usage and we got into things like the last example:
// Turn an array of nodeNames into NodeList of nodes NodeList.from( ["div"], node => document.createElement(node) );
I think I must be missing something. Why is this superior to
NodeList.from(["div"].map(node => document.createElement(node));
Using NodeList.from
returns a NodeList. NodeList.from(...).map(...)
returns an Array.
On 6/24/2013 9:44 AM, Brandon Benvie wrote:
On 6/24/2013 9:42 AM, Domenic Denicola wrote:
That led to further discision of that usage and we got into things like the last example:
// Turn an array of nodeNames into NodeList of nodes NodeList.from( ["div"], node => document.createElement(node) );
I think I must be missing something. Why is this superior to
NodeList.from(["div"].map(node => document.createElement(node));
Using
NodeList.from
returns a NodeList. NodeList.from(...).map(...) returns an Array.
Sorry I misread it! I see your point.
On Jun 24, 2013, at 9:42 AM, Domenic Denicola wrote:
From: Allen Wirfs-Brock [allen at wirfs-brock.com]
My recollection is that we first discussed that the existence of Array.from make this issue somewhat less important because, just as you point out, .from can be used in conjunction with anything that produces an Iterable such as V.from(v.map(val => val * 2))
That led to further discision of that usage and we got into things like the last example:
// Turn an array of nodeNames into NodeList of nodes NodeList.from( ["div"], node => document.createElement(node) );
I think I must be missing something. Why is this superior to
NodeList.from(["div"].map(node => document.createElement(node));
which you gave as an example just a few lines above? In other words, why does
Array.from
accept a second parameter at all?Another question that I don't think got an answer, or if it did I was unable to understand it. Why is "map" singled out for
Array.from
instead of, say, "filter"? It seems arbitrary.
from
esdiscuss/2013-February/028823 On Feb 14, 2013, at 8:47 AM, Nathan Wall wrote:
Hey Allen, thanks for clarifying.
What will happen to other Array methods which currently return Arrays?
filter
is the primary one that comes to mind. Willuint32array.filter((v) => v != 0)
return a Uint32Array? (I think it should behave the same waymap
does.)
filter, splice, splice, reverse, etc. all copy elements that from an existing array or array subclass so the result can/should be the same class is the source array. Map is different because it can produce new element values of an arbitrary kind that may not fit into the same kind of array as the source array
We should probably all go back and review that thread.
On Mon, Jun 24, 2013 at 12:42 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:
From: Allen Wirfs-Brock [allen at wirfs-brock.com]
My recollection is that we first discussed that the existence of Array.from make this issue somewhat less important because, just as you point out, .from can be used in conjunction with anything that produces an Iterable such as V.from(v.map(val => val * 2))
That led to further discision of that usage and we got into things like the last example:
// Turn an array of nodeNames into NodeList of nodes NodeList.from( ["div"], node => document.createElement(node) );
I think I must be missing something. Why is this superior to
NodeList.from(["div"].map(node => document.createElement(node));
which you gave as an example just a few lines above? In other words, why does
Array.from
accept a second parameter at all?
One reason is the extra allocation...
given: let nodeNames = ["div", "span"];
NodeList.from(nodeNames.map(node => document.createElement(node)));
This example is going to produce an extra array (the result of .map()) before it even gets passed into the NodeList.from() call, which will then create a NodeList and then populate it with items from iterable argument.
NodeList.from(["div"], node => document.createElement(node));
This will not create the extra array, only the NodeList.
Another question that I don't think got an answer, or if it did I was unable to understand it. Why is "map" singled out for
Array.from
instead of, say, "filter"? It seems arbitrary.
It's not at all arbitrary: filter isn't an operation used to change the value of the items in the returned iterable.
From: Rick Waldron [waldron.rick at gmail.com]
One reason is the extra allocation...
It's not at all arbitrary: filter isn't an operation used to change the value of the items in the returned iterable.
OK, I think I see. This is because NodeList.prototype.map
behaves differently from NodeList.prototype.filter
: the former returns an Array
, but the latter returns a NodeList
. Thus we need a method of doing "NodeList.prototype.mapAsOwnType
", which we've decided to make static and call NodeList.from
.
Makes sense, I guess? I'll try re-reading threads and meeting notes so I can understand why NodeList.prototype.map
returns an Array
.
On Jun 24, 2013, at 10:22 AM, Domenic Denicola wrote:
From: Rick Waldron [waldron.rick at gmail.com]
One reason is the extra allocation...
It's not at all arbitrary: filter isn't an operation used to change the value of the items in the returned iterable.
OK, I think I see. This is because
NodeList.prototype.map
behaves differently fromNodeList.prototype.filter
: the former returns anArray
, but the latter returns aNodeList
. Thus we need a method of doing "NodeList.prototype.mapAsOwnType
", which we've decided to make static and callNodeList.from
.Makes sense, I guess? I'll try re-reading threads and meeting notes so I can understand why
NodeList.prototype.map
returns anArray
.
It's all coming back to me now and the motivating examples are clear if we use examples with constrained element types such as Typed Arrays.
First, you normally want map and other iterative array functions to return the same type of collect it was applied to:
var smalls = Int8Array.of(34, 78, -150, 127, -3, 12); var negatedSmalls = smalls.map(v=> -v); //negatedSmalltalk should also be a Int8Array
but sometimes you don't
var squaredSmalls_try1 = smalls.map(v=> v*v); //no good if result is Int8Array because many values will be truncated
var squaredSmalls_try2= Int16Array.from(smalls.map(v=> v*v)); // still no good, because intermediate array is Int8Array
var squaredSmalls_try3 = Int16Array.from(smalls, map(v=> v*v)); //the plan is for this to work.
filter, etc. doesn't have this sort of problem because the values placed in the target array are all values that were retrieved from the source array.
Regarding NodeList.prototype.map, it doesn't currently exist and W3C will need to define it if they want it. If if follow the Es6 design it should return a NodeList not an Array.
Thanks Allen. The
var squaredSmalls_try2= Int16Array.from(smalls.map(v=> v*v)); // still no good, because intermediate array is Int8Array
example certainly clears it up for me. Tricky stuff.
I was going to write "it's still a bit weird to me that we overload Array.from
with the mapping functionality", but then I realized this is only weird because of my preconceptions about Array.from
dating back from when we first discussed it on-list.
Taken on its own, without any historical bias, I think it's fine for Array.from
, and more importantly Int16Array.from
etc., to take a mapping function. You derive a new Int16Array
"from" another iterable, optionally via some rule. Makes sense!
On Mon, Jun 24, 2013 at 12:55 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
Thanks Allen. The
var squaredSmalls_try2= Int16Array.from(smalls.map(v=> v*v)); // still no good, because intermediate array is Int8Array
example certainly clears it up for me. Tricky stuff.
I was going to write "it's still a bit weird to me that we overload
Array.from
with the mapping functionality", but then I realized this is only weird because of my preconceptions aboutArray.from
dating back from when we first discussed it on-list.Taken on its own, without any historical bias, I think it's fine for
Array.from
, and more importantlyInt16Array.from
etc., to take a mapping function. You derive a newInt16Array
"from" another iterable, optionally via some rule. Makes sense!
Yup; in other words, Array.from is just the type-converting Array#map (defaulting its callback to the identity function).
On Mon, Jun 24, 2013 at 9:31 AM, Allen Wirfs-Brock <allen at wirfs-brock.com>wrote:
The issues (and discussion) is more complex then the above indicates.
The root issue concerns what should Array producing methods such as Map produce when used in an Array subclass. Should the result be an Array instance (as it currently is in ES5) or should it be a new instance of the subclass. If the latter, how is the actual subclass determined.
If the map
method is inherited and is called directly on the instance of
the inherited class, then probably it's more logical to return the instance
of the same inheriting class (i.e. the V).
To answer your question how the determination of the subclass should happen -- first need to ask what is the [[NativeBrand]] says for "v" instance? Is it still "NativeArray" (shouldn't be IMO, will clarify in the spec later)?
Can you use the [[NativeBrand]] check or at very least constrcutor
property check (which though, not that safe maybe). Will it actually make
sense to set the [[NativeBrand]] to the name of the class?
if (Array.isArray(this)) {
var result = [];
} else {
var result = new this.constructor(<somehow_get_the_default_args>?);
}
But again there is an issue with determination of the default arguments of the constructor, though can fallback to defaults.
On Jun 24, 2013, at 5:21 PM, Dmitry Soshnikov wrote:
On Mon, Jun 24, 2013 at 9:31 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
On Jun 24, 2013, at 8:42 AM, Jason Orendorff wrote:
According to the January 30 meeting notes, Array.from is getting optional map functionality.[1]
This is motivated by the following example:
class V extends Array { constructor(...args) { super(...args); } }
var v, m; v = new V(1, 2, 3); m = v.map(val => val * 2); console.log( m instanceof V ); // false :(
Of course changing Array.from doesn't fix this; v.map() still returns an Array, same as before. And there was already an easy workaround: m = V.from(v.map(val => val * 2));
The issues (and discussion) is more complex then the above indicates.
The root issue concerns what should Array producing methods such as Map produce when used in an Array subclass. Should the result be an Array instance (as it currently is in ES5) or should it be a new instance of the subclass. If the latter, how is the actual subclass determined.
If the
map
method is inherited and is called directly on the instance of the inherited class, then probably it's more logical to return the instance of the same inheriting class (i.e. the V).To answer your question how the determination of the subclass should happen -- first need to ask what is the [[NativeBrand]] says for "v" instance? Is it still "NativeArray" (shouldn't be IMO, will clarify in the spec later)?
Can you use the [[NativeBrand]] check or at very least
constrcutor
property check (which though, not that safe maybe). Will it actually make sense to set the [[NativeBrand]] to the name of the class?if (Array.isArray(this)) { var result = []; } else { var result = new this.constructor(<somehow_get_the_default_args>?); }
But again there is an issue with determination of the default arguments of the constructor, though can fallback to defaults.
[[NativeBrand]] is long gone and regardless won't apply to all subclasses.
Here is how it is currently spec'ed in Array.of and Array.from:
• 1. If IsConstructor(C) is true, then
• a. Let newObj be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len.
• b. Let A be ToObject(newObj).
• 2. Else,
• a. Let A be the result of the abstract operation ArrayCreate with argument len.
while is pretty much equivalent to: class Array extends Object { constructor(...args) { ... } of(...args) { let len = args.length; let a; if (isConstructor(this)) a = new this(len); else a = new Array(len( ... } ... }
In the spec I have a note that it would be better if there was a more explicitly specified way to allocate an empty collection object (of any kind) with a parameterized number of elements. The current spec. for Array.of/from works with any constructor that treats a single numeric argument new call as such a request.
On Mon, Jun 24, 2013 at 5:45 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
[[NativeBrand]] is long gone
Wow, OK, will re-read the final draft then.
Here is how it is currently spec'ed in Array.of and Array.from:
OK, thanks for the clarification. Looks reasonable. The only thing I'm worried is those methods assume (and maybe even force) the inheriting constructors to implement the same API as Array constructor has (that is, either one lenght, or mupltiple entries).
OTOH, subclasses may choose different API, e.g.:
class V extends Array {
constructor(items, config) {
super(...items);
this.isSparse = config.isSparse || false;
...
}
}
On Mon, Jun 24, 2013 at 12:01 PM, Rick Waldron <waldron.rick at gmail.com> wrote:
Another question that I don't think got an answer, or if it did I was unable to understand it. Why is "map" singled out for
Array.from
instead of, say, "filter"? It seems arbitrary.It's not at all arbitrary: filter isn't an operation used to change the value of the items in the returned iterable.
Oh, I didn't get that Array.prototype.filter and the rest are going to be subclass-friendly! Hmm. I want to love this change, but I'm ambivalent. Typed arrays are fixed-length, and DOM NodeLists aren't even constructible. :-\
NodeList is currently experimentally specified to be [ArrayClass]
,
which would cause NodeList.prototype to inherit from
Array.prototype.1 NodeLists would have .filter(). This is
awkward: with the ES5 algorithm, nodelist.filter(pred)
will work,
returning a plain Array; but the proposed subclass-friendly algorithm
will try to create a NodeList---and throw an exception.
If (as I am beginning to suspect) it just makes no sense for NodeList to be a subclass of Array, we should urgently figure out some kind of recommendation for such cases. DOM has more than just the one.
On Tue, Jun 25, 2013 at 11:46 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
If (as I am beginning to suspect) it just makes no sense for NodeList to be a subclass of Array, we should urgently figure out some kind of recommendation for such cases. DOM has more than just the one.
We could give NodeList a constructor. Eventually we want something slightly different from NodeList though, that also exposes some custom methods that make sense for searching through nodes.
On Tue, Jun 25, 2013 at 9:46 AM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
Oh, I didn't get that Array.prototype.filter and the rest are going to be subclass-friendly! Hmm. I want to love this change, but I'm ambivalent. Typed arrays are fixed-length, and DOM NodeLists aren't even constructible. :-\
Wait - the fact that NodeList is not a constructor would save NodeLists in this case. The subclass-friendly methods would still create arrays.
On Jun 25, 2013, at 7:46 AM, Jason Orendorff wrote:
On Mon, Jun 24, 2013 at 12:01 PM, Rick Waldron <waldron.rick at gmail.com> wrote:
Another question that I don't think got an answer, or if it did I was unable to understand it. Why is "map" singled out for
Array.from
instead of, say, "filter"? It seems arbitrary.It's not at all arbitrary: filter isn't an operation used to change the value of the items in the returned iterable.
Oh, I didn't get that Array.prototype.filter and the rest are going to be subclass-friendly! Hmm. I want to love this change, but I'm ambivalent. Typed arrays are fixed-length, and DOM NodeLists aren't even constructible. :-\
see meetings:subclassing_builtins.pdf, meetings:typedarray_status.pdf And the minutes from Jan 2013 TC39 meeting.
Note that TC39 agreed that Typed Arrays will have all of the appropriate Array.prototype methods but we really didn't reach a final conclusion whether that is via actual inheritance from Array.prototype or via (possible) distinct implementations on a TypedArray prototype. I'm working right now on resolving that.
One of the issues is that all the existing Array.prototype method specs Uint32 convert the "array" lengths before processing them. However, TC39 agreed that we want to allow TypedArrays with lengths greater than 2^32-2 . If Typed Arrays are going to inherit methods from Array.prototype we would have to eliminate applying Uint32 to the length. There is uncertainty about whether that would break anything that actually exists.
NodeList is currently experimentally specified to be
[ArrayClass]
, which would cause NodeList.prototype to inherit from Array.prototype.1 NodeLists would have .filter(). This is awkward: with the ES5 algorithm,nodelist.filter(pred)
will work, returning a plain Array; but the proposed subclass-friendly algorithm will try to create a NodeList---and throw an exception.If (as I am beginning to suspect) it just makes no sense for NodeList to be a subclass of Array, we should urgently figure out some kind of recommendation for such cases. DOM has more than just the one.
What we probably need to do is make the algorithm (in my previous message) that chooses the result collection kind a @@method. Then things like NodeList can choose how they want to work.
I'm trying to understand this particular line.
var squaredSmalls_try3 = Int16Array.from(smalls, map(v=> v*v)); //the plan is for this to work.
To me I would expect a complaint that map is undefined. Should map be implied?
var squaredSmalls_try3 = Int16Array.from(smalls, (v=> v*v)); //the plan is for this to work.
Or specified
var squaredSmalls_try3 = Int16Array.from(smalls, Array.map, (v=> v*v)); //the plan is for this to work.
On Tue, Jun 25, 2013 at 3:09 PM, Hudson, Rick <rick.hudson at intel.com> wrote:
I'm trying to understand this particular line.
var squaredSmalls_try3 = Int16Array.from(smalls, map(v=> v*v)); //the plan is for this to work.
To me I would expect a complaint that map is undefined. Should map be implied?
It looks like a leftover from copying this line:
Int16Array.from(smalls.map(v=> v*v));
assume that he means:
Int16Array.from(smalls, v=> v*v);
On Jun 25, 2013, at 12:12 PM, Rick Waldron wrote:
On Tue, Jun 25, 2013 at 3:09 PM, Hudson, Rick <rick.hudson at intel.com> wrote: I'm trying to understand this particular line.
var squaredSmalls_try3 = Int16Array.from(smalls, map(v=> v*v)); //the plan is for this to work.
To me I would expect a complaint that map is undefined. Should map be implied?
It looks like a leftover from copying this line:
Int16Array.from(smalls.map(v=> v*v));
assume that he means:
Int16Array.from(smalls, v=> v*v);
yup, that's what I meant.
Couldn't you just do:
var squaredSmalls = Int16Array.from((v*v for v of smalls));
Or is the allocation of a generator expensive enough to warrant the mapFn argument? Alternatively, is it the need to support a map on a non-iterable "array-like"?
According to the January 30 meeting notes, Array.from is getting optional map functionality.1
This is motivated by the following example:
Of course changing Array.from doesn't fix this; v.map() still returns an Array, same as before. And there was already an easy workaround: m = V.from(v.map(val => val * 2));
This workaround seems superior to the proposal, because it's more generally applicable. The same thing works for the results of .filter(), .concat(), etc. Something very similar works for Set:
Clearly not every possible composition of two primitives needs to be a builtin function. Why this particular one?
Cheers, -j