unzip primitive and/or implicit array-of-objects to object-of-arrays in destructuring
On Tue, May 22, 2012 at 5:57 PM, Herhut, Stephan A < stephan.a.herhut at intel.com> wrote:
While exploring use cases for data-parallel concurrency in JavaScript using the ParallelArray API (see strawman:data_parallelism) I found myself wanting better support for multiple results from a single parallel operation. More precisely, I wanted a neat way to do array-of-objects to object-of-arrays transformations.
The same problem exists with the Array API, probably to a lesser extent as one can always fall back to explicit iterators. So, for sake of simplicity, I will use ordinary Array objects here. Consider the following simple example:
function foo (x) { return {odd: (x%2), twice: 2*x}; }
[1,2,3,4].map(foo);
This will produce an array of objects that have two properties: 'odd' which encodes whether the corresponding source element was odd and 'twice' that gives twice the value of the source element. What I typically want, though, are two arrays containing the odd and twice values, respectively. For instance, I might want to use 'odd' in a consecutive filter operation on 'twice'. One way to achieve this is to write a little unzip helper:
Array.unzip = function unzip(a) { var r = {}; for (var e in a) { for (var k in a[e]) { (r[k] || (r[k]=[]))[e] = a[e][k]; } } return r; }
I'm curious about this example implementation, as it won't do what you've illustrated below - I assume you meant to write Array.prototype.unzip?
which then allows me to conveniently specify the array-of-objects to object-of-arrays conversion like so:
result = [1,2,3,4].map(foo).unzip();
Throw in destructuring as per harmony:destructuring, and you get a pretty workable
{odd, twice} = [1,2,3,4].map(foo).unzip();
Ideally, when implementing this, map should already materialize its result as two separate arrays if its result is only used by an application of unzip. A built in unzip with fixed, well known semantics would make this feasible without the need to analyze what a potential user defined unzip does.
Improving on this some more, one could also extend destructuring to include the required array-of-objects to object-of-arrays transformation and thus directly allow the programmer to write
{odd, twice} = [1,2,3,4].map(foo);
The new semantics of destructuring would have an additional case that, whenever the rhs is an array like value but the lhs is an object pattern, the rhs is first transformed into an object of arrays before the pattern is applied. An implementation is of course free to perform the two steps at once, allowing for optimizations in cases where only parts of the elements are selected, as in
{odd} = [1,2,3,4].map(foo);
Extending destructuring this way does not impact other cases of destructuring. Consider the case where the programmer expected an object but got an array of values. The expression
{odd, twice} = [1,2,3,4];
would still yield odd and twice as undefined. If the lhs is an array pattern, the extended semantics do not apply at all.
To summarize, I propose two things. Either, rather conservatively, that an unzip primitive
primitives in JavaScript are limited to Undefined, Null, Boolean, Number, or String per 4.3.2
On 5/22/12 3:27 PM, "Rick Waldron" <waldron.rick at gmail.com> wrote:
On Tue, May 22, 2012 at 5:57 PM, Herhut, Stephan A <stephan.a.herhut at intel.com> wrote:
While exploring use cases for data-parallel concurrency in JavaScript using the ParallelArray API (see
strawman:data_parallelism, strawman:data_parallelism) I found myself wanting better support for multiple results from a single parallel operation. More precisely, I wanted a neat way to do array-of-objects to object-of-arrays transformations.
The same problem exists with the Array API, probably to a lesser extent as one can always fall back to explicit iterators. So, for sake of simplicity, I will use ordinary Array objects here. Consider the following simple example:
function foo (x) { return {odd: (x%2), twice: 2*x}; }
[1,2,3,4].map(foo);
This will produce an array of objects that have two properties: 'odd' which encodes whether the corresponding source element was odd and 'twice' that gives twice the value of the source element. What I typically want, though, are two arrays containing the odd and twice values, respectively. For instance, I might want to use 'odd' in a consecutive filter operation on 'twice'. One way to achieve this is to write a little unzip helper:
Array.unzip = function unzip(a) { var r = {}; for (var e in a) { for (var k in a[e]) { (r[k] || (r[k]=[]))[e] = a[e][k]; } } return r; }
I'm curious about this example implementation, as it won't do what you've illustrated below - I assume you meant to write Array.prototype.unzip?
Indeed, well spotted.
which then allows me to conveniently specify the array-of-objects to object-of-arrays conversion like so:
result = [1,2,3,4].map(foo).unzip();
Throw in destructuring as per harmony:destructuring, harmony:destructuring, and you get a pretty workable
{odd, twice} = [1,2,3,4].map(foo).unzip();
Ideally, when implementing this, map should already materialize its result as two separate arrays if its result is only used by an application of unzip. A built in unzip with fixed, well known semantics would make this feasible without the need to analyze what a potential user defined unzip does.
Improving on this some more, one could also extend destructuring to include the required array-of-objects to object-of-arrays transformation and thus directly allow the programmer to write
{odd, twice} = [1,2,3,4].map(foo);
The new semantics of destructuring would have an additional case that, whenever the rhs is an array like value but the lhs is an object pattern, the rhs is first transformed into an object of arrays before the pattern is applied. An implementation is of course free to perform the two steps at once, allowing for optimizations in cases where only parts of the elements are selected, as in
{odd} = [1,2,3,4].map(foo);
Extending destructuring this way does not impact other cases of destructuring. Consider the case where the programmer expected an object but got an array of values. The expression
{odd, twice} = [1,2,3,4];
would still yield odd and twice as undefined. If the lhs is an array pattern, the extended semantics do not apply at all.
To summarize, I propose two things. Either, rather conservatively, that an unzip primitive
primitives in JavaScript are limited to Undefined, Null, Boolean, Number, or String per 4.3.2
I was thinking of a built-in method of Array, rather than a primitive in the ES spec sense.
Stephan
Herhut, Stephan A wrote:
Either, rather conservatively, that an unzip primitive be added to Array/ParallelArray
+1
or that destructuring be extended such that if it encounters a lhs object pattern and a rhs evaluating to an array of objects that it be destructured into objects of arrays corresponding to variables specified in the lhs.
-1 to implicit unzip. This is fragile (EIBTI) and in fact arrays are objects, so breaking the equivalence of {0:x,1:y} and [x,y] (note no need to get 'length' in the latter pattern) seems unwarranted.
On 5/22/12 4:31 PM, "Brendan Eich" <brendan at mozilla.org> wrote:
Herhut, Stephan A wrote:
Either, rather conservatively, that an unzip primitive be added to Array/ParallelArray
+1
or that destructuring be extended such that if it encounters a lhs object pattern and a rhs evaluating to an array of objects that it be destructured into objects of arrays corresponding to variables specified in the lhs.
-1 to implicit unzip. This is fragile (EIBTI) and in fact arrays are objects, so breaking the equivalence of {0:x,1:y} and [x,y] (note no need to get 'length' in the latter pattern) seems unwarranted.
I agree, semantically arrays are just objects of a certain form. However, by using an array instead of an object, I, the programmer, express an intent of what I want to happen. For instance, I would expect constant time lookup (I know, not always true in JS). So having pattern matching work differently on arrays than on general objects would not come as a big surprise to me.
Having said that and given the identity crisis arrays have in JavaScript, I understand your EIBTI comment.
Regarding your comment on length: I don't get that. If I do
let [x,y] = e;
where is length getting involved (other than in the destructuring algorithm)?
Stephan
Herhut, Stephan A wrote:
Regarding your comment on length: I don't get that. If I do
let [x,y] = e;
where is length getting involved (other than in the destructuring algorithm)?
It should not be involved in that case. But consider a spread pattern:
let [x, y, ...z] = e;
in this case, z should destructure an array containing all the elements (if any) in e from index 2 up to but not including e.length. So a 'length' get is needed in this case.
We had an argument last year, IIRC, about whether all array patterns should therefore get 'length' once, before any other gets from e's value, just to be "consistent".
I recall prevailing in my "no" answer :-P. We want pay-as-you-go, and ...z is a special form in the pattern that connotes 'length'-get. Without a spread pattern, no 'length' get.
While exploring use cases for data-parallel concurrency in JavaScript using the ParallelArray API (see strawman:data_parallelism) I found myself wanting better support for multiple results from a single parallel operation. More precisely, I wanted a neat way to do array-of-objects to object-of-arrays transformations.
The same problem exists with the Array API, probably to a lesser extent as one can always fall back to explicit iterators. So, for sake of simplicity, I will use ordinary Array objects here. Consider the following simple example:
function foo (x) { return {odd: (x%2), twice: 2*x}; }
[1,2,3,4].map(foo);
This will produce an array of objects that have two properties: 'odd' which encodes whether the corresponding source element was odd and 'twice' that gives twice the value of the source element. What I typically want, though, are two arrays containing the odd and twice values, respectively. For instance, I might want to use 'odd' in a consecutive filter operation on 'twice'. One way to achieve this is to write a little unzip helper:
Array.unzip = function unzip(a) { var r = {}; for (var e in a) { for (var k in a[e]) { (r[k] || (r[k]=[]))[e] = a[e][k]; } } return r; }
which then allows me to conveniently specify the array-of-objects to object-of-arrays conversion like so:
result = [1,2,3,4].map(foo).unzip();
Throw in destructuring as per harmony:destructuring, and you get a pretty workable
{odd, twice} = [1,2,3,4].map(foo).unzip();
Ideally, when implementing this, map should already materialize its result as two separate arrays if its result is only used by an application of unzip. A built in unzip with fixed, well known semantics would make this feasible without the need to analyze what a potential user defined unzip does.
Improving on this some more, one could also extend destructuring to include the required array-of-objects to object-of-arrays transformation and thus directly allow the programmer to write
{odd, twice} = [1,2,3,4].map(foo);
The new semantics of destructuring would have an additional case that, whenever the rhs is an array like value but the lhs is an object pattern, the rhs is first transformed into an object of arrays before the pattern is applied. An implementation is of course free to perform the two steps at once, allowing for optimizations in cases where only parts of the elements are selected, as in
{odd} = [1,2,3,4].map(foo);
Extending destructuring this way does not impact other cases of destructuring. Consider the case where the programmer expected an object but got an array of values. The expression
{odd, twice} = [1,2,3,4];
would still yield odd and twice as undefined. If the lhs is an array pattern, the extended semantics do not apply at all.
To summarize, I propose two things. Either, rather conservatively, that an unzip primitive be added to Array/ParallelArray or that destructuring be extended such that if it encounters a lhs object pattern and a rhs evaluating to an array of objects that it be destructured into objects of arrays corresponding to variables specified in the lhs.
Stephan