Why does Array.from also take a mapFn?

# Jason Orendorff (11 years ago)

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));

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?

Cheers, -j

# Allen Wirfs-Brock (11 years ago)

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.

# Brandon Benvie (11 years ago)

For reference, this is the thread: esdiscuss/2013-February/028661

# Domenic Denicola (11 years ago)

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.

# Brandon Benvie (11 years ago)

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.

# Brandon Benvie (11 years ago)

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.

# Allen Wirfs-Brock (11 years ago)

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. Will uint32array.filter((v) => v != 0) return a Uint32Array? (I think it should behave the same way map 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.

# Rick Waldron (11 years ago)

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.

# Domenic Denicola (11 years ago)

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.

# Allen Wirfs-Brock (11 years ago)

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 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.

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.

# Domenic Denicola (11 years ago)

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!

# Tab Atkins Jr. (11 years ago)

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 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!

Yup; in other words, Array.from is just the type-converting Array#map (defaulting its callback to the identity function).

# Dmitry Soshnikov (11 years ago)

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.

# Allen Wirfs-Brock (11 years ago)

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.

# Dmitry Soshnikov (11 years ago)

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;
    ...
  }
}
# Jason Orendorff (11 years ago)

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.

# Anne van Kesteren (11 years ago)

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.

-- annevankesteren.nl

# Jason Orendorff (11 years ago)

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.

# Allen Wirfs-Brock (11 years ago)

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.

# Hudson, Rick (11 years ago)

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.
# Rick Waldron (11 years ago)

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);
# Allen Wirfs-Brock (11 years ago)

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.

# Ron Buckton (11 years ago)

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"?