Array.prototype.toObjectByProperty( element=>element.property )
On 2017-08-05 20:18, Naveen Chawla wrote:
I've often needed to cache array elements by a unique key each element has, for quick access.
This is a shortcut:
const elementsById = elements.toObjectByProperty(element=>element.id);
That looks like what Array.prototype.map can already do.
On Sat, Aug 5, 2017 at 11:18 AM, Naveen Chawla <naveen.chwl at gmail.com> wrote:
I've often needed to cache array elements by a unique key each element has, for quick access.
Yes, please. I've needed this in every codebase I've ever worked on. For
me, having it in the standard lib would be a win. (Yes, you can easily do
it by (ab)using reduce
, but A) It's arguably an abusage [one I'm
increasingly resigning myself to], and B) Why keep re-writing it?)
Some off-the-cuff suggestions:
-
Put it on
Object
:Object.from
. -
Have a Map version:
Map.from
. -
Accept any iterable, not just an array.
-
Accept a string, Symbol, or callback: If a string or Symbol, use the matching property as the key for the result. If a callback, use the callback's return value (your example).
-
Optionally accept the object or map rather than creating a new one.
So something like:
Object.from(iterable, indexer, target = Object.create(null))
and
Map.from(iterable, indexer, target = new Map)
-- T.J. Crowder
On Sat, Aug 5, 2017 at 12:22 PM, Lachlan Hunt <lachlan.hunt at lachy.id.au> wrote:
On 2017-08-05 20:18, Naveen Chawla wrote:
I've often needed to cache array elements by a unique key each element has, for quick access.
That looks like what Array.prototype.map can already do.
No, the end result is an object, not an array. It probably would have
helped if Naveen had included an example. Here I'm using Object.from
(see
my previous email) rather than toObjectByProperty
:
const a = [{id: "tjc", name: "T.J. Crowder"}, {id: "nc", name: "Naveen
Chawla"}, {id: "lh", name: "Lachlan Hunt"}];
const index = Object.from(a);
console.log(index);
would yield:
{
tjc: {id: "tjc", name: "T.J. Crowder"},
nc: {id: "nc", name: "Naveen Chawla"},
lh: {id: "lh", name: "Lachlan Hunt"}
}
-- T.J. Crowder
On Sat, Aug 5, 2017 at 12:41 PM, T.J. Crowder <tj.crowder at farsightsoftware.com> wrote:
Apologies, better proofreading required, left out the "id"
argument:
const a = [{id: "tjc", name: "T.J. Crowder"}, {id: "nc", name: "Naveen
Chawla"}, {id: "lh", name: "Lachlan Hunt"}];
const index = Object.from(a, "id");
console.log(index);
-- T.J. Crowder
On 2017-08-05 21:47, T.J. Crowder wrote:
On Sat, Aug 5, 2017 at 12:41 PM, T.J. Crowder <tj.crowder at farsightsoftware.com> wrote:
Apologies, better proofreading required, left out the
"id"
argument:const a = [{id: "tjc", name: "T.J. Crowder"}, {id: "nc", name: "Naveen Chawla"}, {id: "lh", name: "Lachlan Hunt"}]; const index = Object.from(a, "id"); console.log(index);
OK, I misunderstood the original request. But even so, as you noted in your previous mail, Array.prototype.reduce can handle it.
a.reduce((obj, value) => (obj[value.id] = value, obj), {})
But basically, is the proposal for a native version of Lodash's _.keyBy() method?
On Sat, Aug 5, 2017 at 1:05 PM, Lachlan Hunt <lachlan.hunt at lachy.id.au> wrote:
But basically, is the proposal for a native version of Lodash's
_.keyBy()
method?
Well, that's how I read it, yes; along those lines anyway.
-- T.J. Crowder
Inline.
On Saturday, August 5, 2017 1:35:35 PM CEST T.J. Crowder wrote:
On Sat, Aug 5, 2017 at 11:18 AM, Naveen Chawla
<naveen.chwl at gmail.com> wrote:
I've often needed to cache array elements by a unique key each element has, for quick access.
Yes, please. I've needed this in every codebase I've ever worked on. For me, having it in the standard lib would be a win. (Yes, you can easily do it by (ab)using
reduce
, but A) It's arguably an abusage [one I'm increasingly resigning myself to], and B) Why keep re-writing it?)
Why do you consider this an abuse of reduce
? The problem at hand is to
reduce an iterable to a single value with certain properties. And I would like
to think that reducing is what reduce
is supposed to do. ;)
Could you please elaborate on that?
FWIW, while I find needs like this common, too, where Map is sensible instead of Object, it does come out pretty clean:
const a = [
{id: "tjc", name: "T.J. Crowder"},
{id: "nc", name: "Naveen Chawla"},
{id: "lh", name: "Lachlan Hunt"}
];
const index = new Map(a.map(member => [ member.name, member ]));
Although I’m also puzzled by the suggestion that reducing to an object is an abuse, I do find I wish there were a complement to Object.entries
:
// Object to pairs, and therefore map, is simple:
const map = new Map(Object.entries(obj));
// Converting back is also simple ... but not exactly expressive, if you inline it:
[ ...map ].reduce((acc, [ key, val ]) => Object.assign(acc, { [key]: val }), {});
Something like Object.fromEntries
would not provide as much sugar for the OP case as toObjectByProperty
, but it gets pretty close and has the advantage of being more generic; toObjectByProperty
strikes me as rather specific for a built-in, especially since one might want to map by a derived value rather than a property. Both map<->object and array<->object cases would become more expressive — plus it follows pretty naturally from the existence of Object.entries
that there might be a reverse op.
Object.fromEntries(a.map(member => [ member.name, member ]));
In other words, Object.fromEntries(Object.entries(obj))
would be equivalent in effect to Object.assign({}, obj)
.
Would that adequately address this case you think? My sense is that it’s better to supply generic helpers before more specific helpers when it comes to built-ins.
I have a better idea: how about Object.from(iter)
, where iter
is
an iterable of [key, value]
pairs?
const a = [
{id: "tjc", name: "T.J. Crowder"},
{id: "nc", name: "Naveen Chawla"},
{id: "lh", name: "Lachlan Hunt"}
];
const object = Object.from(a.map(o => [o.id, o.name]))
We already have Object.entries(object)
, returning an array of [key, value]
pairs, so this would effectively be the inverse of that -
Object.from(Object.entries(object))
would be equivalent to
Object.assign({}, object)
assuming native prototypes are unmodified.
(Yes, it's like _.zipObject(pairs)
from Lodash)
Isiah Meadows me at isiahmeadows.com
Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com
If anything were to be added, it would surely be a static method that
took an iterable of entries and returned an object, and it would surely
have the semantic that new Set(Object.entries(thisThing(entries)))
and
new Set(entries)
were two conceptually equal sets (iow, that each
corresponding entry's key were ===, modulo ToPropertyKey conversion, and
each corresponding entry's value were ===).
I'm not necessarily proposing this at all or any time soon, but one thing that would help me or a future champion (if proposed) is if interested parties wrote up a convincing proposal readme - rationale, similar patterns in the ecosystem, motivating use cases, etc.
@Isiah I think you and I just described the identical thing (except that
I’d put fromEntries
where you put from
) — and it’s a subset of the
overloaded proposed solution from Crowder above. That three people
responded with the same thing or a variation of it suggests that this is
indeed a gap where a thing intuitively ought to exist.
@Harband Unless somebody else wants to, I’d be interested in trying my hand at assembling a proper proposal. I think I’ve spent enough time in the spec & various proposals at this point that I might not totally butcher it.
Awesome. I didn’t know about the upcoming template, so I may have jumped the gun haha:
It’s quite primitive though, no formal algorithm / ecmarkup or anything. I’ll keep an eye on the template project and update accordingly once that’s ready, thanks.
Edit: oops, this was in response to a message from Jordan Harband that I didn’t realize was off-list (it showed up in the same email thread in gmail), hence the odd first sentence.
On Sat, Aug 5, 2017 at 9:42 PM, Darien Valentine <valentinium at gmail.com> wrote:
FWIW, while I find needs like this common, too, where Map is sensible instead of Object, it does come out pretty clean:
const a = [ {id: "tjc", name: "T.J. Crowder"}, {id: "nc", name: "Naveen Chawla"}, {id: "lh", name: "Lachlan Hunt"} ]; const index = new Map(a.map(member => [ member.name, member ]));
And:
On Sun, Aug 6, 2017 at 7:31 PM, Isiah Meadows <isiahmeadows at gmail.com> wrote:
I have a better idea: how about
Object.from(iter)
, whereiter
is an iterable of[key, value]
pairs?
For the common case I have, that would require an additional transform
producing a lot of unnecessary temporary objects. I basically never
have [ [key, value], [key, value] ...]
as my starting point where I
want this.
On Sat, Aug 5, 2017 at 9:42 PM, Darien Valentine <valentinium at gmail.com> wrote:
Although I’m also puzzled by the suggestion that reducing to an object is an abuse...
Let's ignore that, sorry; it's purely a distraction in this conversation.
toObjectByProperty
strikes me as rather specific for a built-in, especially since one might want to map by a derived value rather than a property.
Naveen's original post shows a callback that returns the key to use. I
also flag up using a callback to determine the value in my earlier
post on this. (In my post, I cater to the common case of a specific
property known in advance, but also allow for a callback; like
String#replace
's second parameter.)
Would that adequately address this case you think?
For me, no; an unnecessary additional transform and too much memory churn.
My sense is that it’s better to supply generic helpers before more specific helpers when it comes to built-ins.
In general, absolutely. But where the abstraction takes us into
unnecessary complexity and performance degradation, slightly more
specificity is fine in my view. I don't think my suggested
Object.from
and Map.from
are too specific.
-- T.J. Crowder
For me, no; an unnecessary additional transform and too much memory churn.
Interesting. I’ve included a note in the 'concerns' section on that
proposal regarding the possibility of accepting a mapping function (like
Array.from
). That could avoid the extra array creation. However I suspect
the idea of accepting a string or symbol key rather than a function is
unlikely to gain traction — for one, there’s nothing like it presently in
the many places there could be, and perhaps more importantly, that
overloading represents an additional cost for every use case, even if it
might reduce the cost for one. We’re talking very small perf differences
here of course, but one of the reasons many lodash tools are not
particularly fast is the overhead that comes from supporting heavy
overloading.
Assuming that proposal isn’t a flop, please feel free to open an issue on the repo making the case for the map argument (in whatever form) — that could help measure whether that’s something most people expect to see.
On Mon, Aug 7, 2017 at 2:23 PM, Darien Valentine <valentinium at gmail.com> wrote:
For me, no; an unnecessary additional transform and too much. memory churn.
However I suspect the idea of accepting a string or symbol key rather than a function is unlikely to gain traction — for one, there’s nothing like it presently in the many places there could be
Where are you thinking of?
...and perhaps more importantly, that overloading represents an additional cost for every use case, even if it might reduce the cost for one.
That's a fair concern, although the test is trivially simple and very fast.
I'll flag up again the precedent of String#replace
. In my experience, the
string use case is overwhelmingly the common one. So slowing down the
common case because of edge cases outweighs that concern for me. In spec
terms, I'd test first for string, then function, then Symbol. (In polyfill
terms, it'd be a switch
on typeof
.)
Assuming that proposal isn’t a flop, please feel free to open an issue on the repo making the case for the map argument...
I don't think your Object.fromEntries
isn't similar enough to what I
want. I'll create a proposal for what I want in a few days after more
feedback has come in.
You seem to have had some private reply about an upcoming proposal template...?
-- T.J. Crowder
Where are you thinking of? [re: other methods that could take overloaded/shorthand map/predicate args, but do not]
These could also accept strings for property mapping in the same manner you proposed, but do not currently:
Array.from
Array.prototype.every
Array.prototype.filter
Array.prototype.map
Array.prototype.some
new Map
new Set
Mind, I’m not arguing that it isn’t a useful idea. Where lodash has equivalents for the above, strings are permitted, and I’m sure people make use of that functionality.
I think the spot where we might disagree is the assessment that "the string case is overwhelmingly the common one." This doesn’t sound quite right to me. That’s not been true in my experience, though I don’t have any data to say concretely one way or another. In any case, I’m not out to block anything if smarter people think it’s a good idea.
I'll create a proposal for what I want in a few days after more feedback has come in.
Would you consider the method you’ll be proposing to be "instead of" a reverse-of-object-entries type proposal, or is it orthogonal to that?
You seem to have had some private reply about an upcoming proposal template...?
Yeah — it doesn’t exist yet, however.
On Mon, Aug 7, 2017 at 4:17 PM, Darien Valentine <valentinium at gmail.com> wrote:
Where are you thinking of? [re: other methods that could take
overloaded/shorthand map/predicate args, but do not]
These could also accept strings for property mapping in the same manner
you proposed, but do not currently:
Array.from
Array.prototype.every
- ...
I think it comes down to what the majority use case is. For most of those
I've never used just a single property access. But I certainly have for
map
and filter
, I see your point there. It's not been anything like my
majority use case for those two, but definitely not rare.
I think the spot where we might disagree is the assessment that "the string case is overwhelmingly the common one." This doesn’t sound quite right to me. That’s not been true in my experience...
:-) Your mileage may vary, but I'm surprised by that; surely you use maps
keyed by a property of the object they map to? The times I've wanted to
generate the key rather than just pluck it can be counted on one finger
(since Object.create
; prior to that, I was prefixing names in index
objects to avoid Object.prototype
collisions, back in the day). Whereas
wanting to use an existing property as the index key I've done hundreds of
times if not more. For instance, an app with a list of...well, almost
anything, where I want access both as a list (for iteration; showing the
list) and random access by ID (for showing details in response to a pick).
I'll create a proposal for what I want in a few days after more feedback has come in.
Would you consider the method you’ll be proposing to be "instead of" a reverse-of-object-entries type proposal, or is it orthogonal to that?
Ortogonal I think. Your Object.fromEntries
works with an iterable of
[key, value]
pairs (or, I'm guessing, a callback that can produce [key, value]
pairs). My Object.from
works with an iterable of any kind of
object where the object itself is expected to be the value. I think trying
to shoehorn both in to one function would over-complicate it.
-- T.J. Crowder
The concept of using a 2-element array to represent a key and value is unique to Maps and if I'm not mistaken, exists purely because there wasn't a tight, compact alternative that could house a key and value where the key could be an object.
However, if we are going straight from iterable to Object, I don't think we should be propagating the "[key, value] as an array" pattern. It's unclean partly because it's 2 elements max and IDEs can't inherently show warnings if you accidentally added more, unless it has some intelligence about the method itself. Partly also because the arrays are not immediately readable unless you already know the that the arrays mean [key, value].
I'm not convinced that Maps have any real value at all right now vs objects (despite what the documentation says): currently they only offer the ability to have objects as keys but without custom key equality (only ===), and you lose the elegant square bracket syntax for accessing elements, among other things, so it's unclear to me how compelling the use of Maps vs objects is at all in real world scenarios, so I'm not sure anything about Maps should be a pattern for other language features.
Iterables are very generic, since they can include generators, sets etc. Objects are a fundamental construct, so it makes sense to be able to cleanly transition between these structures.
Adding the idea to iterable in general (instead of just Array) would look like e.g.:
const cache = iterable.toObjectByProperty(item=>item.id)
On Mon, Aug 7, 2017 at 10:24 PM, Naveen Chawla <naveen.chwl at gmail.com>
wrote:
However, if we are going straight from iterable to Object, I don't think we should be propagating the "[key, value] as an array" pattern.
Consistency is good, and especially better than for example adding new
syntax. Subjectively not a big fan of doing single pair computed key
objects either: return { [key]: value }
vs return [key, value]
. Arrays
also have a better set of standard library functions available which makes
preprocessing easier, for example reversing keys and values.
It's unclean partly because it's 2 elements max and IDEs can't inherently show warnings if you accidentally added more, unless it has some intelligence about the method itself. Partly also because the arrays are not immediately readable unless you already know the that the arrays mean [key, value].
You're probably not referring to hand-writing lists of key-value pairs (in which case you should just use an object literal to begin with), but yes, text editors inherently don't help much with code validity to begin with, but type systems such as flow make it pretty easy1 to catch this sort of thing:
/* @flow */
function objectFrom<KeyType, ValueType>(
iterable : Iterable<[KeyType, ValueType]>
) : {
[key : KeyType]: ValueType
} {
const object : { [key : KeyType]: ValueType } = {};
for (const [key, value] of iterable) {
object[key] = value;
}
return object;
}
objectFrom([["key", 2], [null, "value"], [1, 3]]) // <- all good
objectFrom([["key", "value", "other"]]) // <- ERROR: Tuple arity mismatch.
This tuple has 3 elements and cannot flow to the 2 elements of...
objectFrom([1,2,3,4,5].map(x => [x, x])) // <- all good
objectFrom([1,2,3,4,5].map(x => Array(x))) // <- // <- ERROR: Tuple arity
mismatch...
IMO it's generally a good idea to use stronger correctness guarantees than editor syntax highlighting / validation anyway.
Regarding Map.from(...)
, not sure how that would be useful since the map
constructor already accepts an iterator. Maybe when passing to something
that expects a function, but then adding arity (for example a mapper
function) becomes a hazard. The partial application proposal might help
with this tho: x.map(Map.from(?))
.
I'd be happy to see something like Object.from(iterable)
and maybe even
Map.from(iterable)
in the core library - simple, generic and common. Not
so convinced about the mapper functions - if an engine doesn't bother
optimizing away the extra allocation in Object.from(x.map(fn))
I don't
see why it would bother optimizing Object.from(x, fn)
either. Please
correct me if and how I'm wrong though.
The concept of using a 2-element array to represent a key and value is unique to Maps
The entries pattern is not unique to Map
— it’s also used by
Object.entries
, Set.entries
, and Array.entries
— e.g.
for (const [ index, member ] of array.entries()) ...
A bit tangential, but in the case of Map
, it’s perhaps notable that the
contract for "entry" is not "an array of two and only two members". It is
"any object at all", with the implied expectation that you’d want
properties keyed by "0" and "1" defined on it in order for it to be useful.
Regardless of one’s thoughts on Map
(which I use quite a bit personally),
the "entries" pattern is well established in ES. (I’d also point out that
adding a method for composition from entries does not preclude supporting
other approaches.)
Very interesting!
Set.entries() is interesting - it has [value, value] with the justification (according to MDN) "to keep the API similar to the Map object".
Array entries() - it's really unclear why this would ever be used, do you have any idea?
Object entries() is another strange case. Iterating over Object.keys with key=> leaves myObject[key] to access the value, whereas iterating over
Object.entries with entry=> requires entry[0] and entry[1] which seems more
verbose for access, and less readable! So do you know why this was introduced?
All seem motivated by Map. And I think the pattern is unhealthy, for the reasons given before.
Sometimes we need to look at how code looks out there in production, to
appreciate the benefits of a language feature. Imagine the mapping of an
array passed into a function (or a reduce), repeated in code for several
arrays that need caching for quick access - obviously you would expect that
to be factored out into a toObjectByProperty
function in code, right?
That's what I'm talking about. Now imagine that transition function offered
by iterable - an extra dependency useful across multiple projects is gone.
It's only nice. Isn't it?
Entries are an established pattern. Relitigating it isn't going to be useful. Something like this would only make sense if it accepted either an iterable of entries, or an array of entries (which is strictly less useful than taking an iterable) - no other input value would make sense to me.
Object entries() is another strange case. Iterating over Object.keys with
key=> leaves myObject[key] to access the value, whereas iterating over
Object.entries with entry=> requires entry[0] and entry[1] which seems more
verbose for access, and less readable! So do you know why this was introduced?
Keep in mind that the pair syntax plays quite nicely with destructuring, so assuming the iteration you're describing is something like
for (const key of Object.keys(myObject)) {
const value = myObject[key];
// do stuff
}
I at least think it's much more readable to do
for (const [key, value] of Object.entries(myObject)) {
// do stuff
}
Array entries() - it's really unclear why this would ever be used, do you
have any idea?
The same is nice for iterating arrays when you need the index
for (const [i, value] of ['one', 'two', 'three', 'four'].entries()) {
// do stuff
}
Imagine the mapping of an array passed into a function (or a reduce),
repeated in code for several arrays that need caching for quick access -
obviously you would expect that to be factored out into a
toObjectByProperty
function in code, right? That's what I'm talking
about. Now imagine that transition function offered by iterable - an extra
dependency useful across multiple projects is gone. It's only nice. Isn't
it?
Generally at least for me I've transitioned to using Map objects for this
type of lookup. It's slightly longer to use .get
, but at least for me it
hasn't been particularly troublesome. There are certainly performance
arguments to be made for polyfilled implementations, but that will only
decrease over time.
On Tue, 8 Aug 2017 at 03:25 Jordan Harband <ljharb at gmail.com> wrote:
Entries are an established pattern. Relitigating it isn't going to be useful. Something like this would only make sense if it accepted either an iterable of entries, or an array of entries (which is strictly less useful than taking an iterable) - no other input value would make sense to me.
They are established as a feature in the language. I am not trying to relitigate that. I'm simply saying that when transitioning from a typical iterable to an object we shouldn't be forced into it as an intermediary, besides which that doesn't offer any advantage over the reduce mentioned earlier in this thread, and even has worse performance. The [key, value] pattern was originally introduced for Maps to allow objects as keys, which isn't even valid in an object, besides the pattern not being a tight structure in of itself.
On Tue, 8 Aug 2017 at 03:28 Logan Smyth <loganfsmyth at gmail.com> wrote:
Object entries() is another strange case. Iterating over Object.keys with key=> leaves myObject[key] to access the value, whereas iterating over Object.entries with entry=> requires entry[0] and entry[1] which seems more verbose for access, and less readable! So do you know why this was introduced?
Keep in mind that the pair syntax plays quite nicely with destructuring, so assuming the iteration you're describing is something like
for (const key of Object.keys(myObject)) { const value = myObject[key]; // do stuff }
I at least think it's much more readable to do
for (const [key, value] of Object.entries(myObject)) { // do stuff }
Yes unfortunately this doesn't apply to forEach
, which I use much more
frequently because it allows chaining after map
, filter
, sort
,
concat
etc. and because it's slightly less verbose. I only use for..of
when I need to break out of a loop, and which frankly I'll stop using
once/if takeWhile
/skipWhile
are introduced.
forced into it as an intermediary
Fortunately, no force is involved :)
T.J. Crowder indicated an intention to draft a proposal that I believe aligns with your ideas for converting objects to a single object by key with an all-in-one map+compose method. It is not necessary for there to be only a single proposal in this space — aside from the fact that having diversity of ideas on the table is healthy, the two ideas do not seem to be in conflict with each other.
has worse performance
It’s a bit hard to speak about the performance of methods no engine has
implemented, but you might be overestimating the cost of
Array.prototype.map
.
The [key, value] pattern was originally introduced for Maps to allow objects as keys [...]
You described earlier a previous unfamiliarity with the various entries
methods on collections. This might have contributed to an impression that
entries are a concern of Map specifically. While Map introduced entries, in
real-world code, I think it’s safe to say confidently that the frequency of
use
of Object.entries
for iteration (as illustrated in Logan Smyth’s earlier
comment) and transformative operations (as mentioned in Jussi Kalliokoski’s
comment) dwarfs that of its use for map initialization.
Yes unfortunately this doesn't apply to forEach, [...]
Are you referring to destructuring? It does:
const obj = { apples: 2, bananas: 17, blueberries: 9 };
const numberOfFruitsThatStartWithB = Object
.entries(obj)
.filter(([ key ]) => key.startsWith('b'))
.reduce((acc, [ , value ]) => acc + value, 0);
If you have an object, you can already Object.assign({}, object)
, or
Object.defineProperties({}, Object.getOwnPropertyDescriptors(object))
,
with no intermediary, as of ES2015 and ES2017 respectively.
The value here for something new would be
Object.fromEntries(Object.entries(object).map(…).filter(…))
or similar.
I haven't seen any suggestions for anything different than those three patterns; am I missing something?
I haven't seen any suggestions for anything different than those three patterns; am I missing something?
The draft I assembled does follow that pattern, but there was another approach suggested which is not concerned with entries and is instead similar to lodash’s keyBy method; it also permits supplying a target object rather than always creating a new object.
Gah, I always manage to screw up the recipient field on these threads.
Another benefit of iterable toObjectByProperty(element=>element.id) is that
it can be placed at the end of a transformation chain of map
sort
filter
concat
etc.:
const
cache =
sourceIterable
.filter(item=>item.isValid)
.toObjectByProperty(item=>item.id)
It'd be great to know if anyone from TC39 is going to propose these ideas (could I submit a README.md in order to help? If so where?)
On Tue, Aug 8, 2017 at 7:18 AM, Naveen Chawla <naveen.chwl at gmail.com> wrote:
It'd be great to know if anyone from TC39 is going to propose these ideas (could I submit a README.md in order to help? If so where?)
Proposals come from the community (of which TC39 members are a part). What you've done so far is a suggestion. For it to proceed, someone has to create a proper proposal for it.
Darien's already created one based on his initial interpretation of your idea. I intend to do a separate one later this week when I have time and after more reflection/feedback, based on what I've wanted for several years -- which is, I think, closer to what you originally suggested. As he said, the two may or may not compete; I don't think they do, I think they complement one another.
Then we'll see if a TC39 committee member decides to champion either, both, or neither of them (or any other that may also be made).
-- T.J. Crowder
I'd like to propose an enhancement to my proposal:
const cache = iterable.toObject(item=>item.key, item=>item); //2nd param is
optional, default shown
It offers the same functionality, but in addition a second optional parameter for the "value" in case you want something other than the array element as the value. By default, if the second parameter isn't populated, it should use the array element itself, like the example call redundantly passes.
On Tue, Aug 8, 2017 at 9:39 AM, Naveen Chawla <naveen.chwl at gmail.com> wrote:
I'd like to propose an enhancement to my proposal: ... It offers the same functionality, but in addition a second optional parameter for the "value" in case you want something other than the array element as the value. By default, if the second parameter isn't populated, it should use the array element itself, like the example call redundantly passes.
This is part of why Darien's proposal and my suggestion from my first
reply in this thread are complementary: That's exactly what you get
with Darien's, by returning [key, value]
. Conflating them makes each of
them unnecessarily complicated in my view.
-- T.J. Crowder
Yeah except in what I'm saying it's optional
On Tue, Aug 8, 2017 at 10:01 AM, Naveen Chawla <naveen.chwl at gmail.com>
wrote:
Yeah except in what I'm saying it's optional
My point is you'd use the right tool for the job at hand, rather than having a single tool with a bunch of options making it complex to explain, use, and optimize. If you review my earlier suggestion and the back-and-forth with Darien, I think you'll see what I mean.
-- T.J. Crowder
Philosophically I agree completely with this principle.
This does not have a bunch of options:
iterable.toObject(keyFromElement[, valueFromElement])
What I proposed has only 1 variant: the fact that valueFromElement
has a
default if you don't provide it.
Objects as they are are simple per insertion: 1. key, 2. value.
It's not straightforward to do my (latest) proposal without it, in particular if the object property values you want doesn't contain the key itself because it's derived from a different piece of data from which it doesn't exist.
Furthermore, if you use entries, this allows [key, value]
entries with
object keys to be transformed into objects (which is not allowed by
Object.fromEntries
):
const cache = entries.toObject(entry=>entry[0].type, entry=>entry[1]);
On Tue, Aug 8, 2017 at 11:39 AM, Naveen Chawla <naveen.chwl at gmail.com>
wrote:
Furthermore, if you use entries, this allows
[key, value]
entries with object keys to be transformed into objects (which is not allowed byObject.fromEntries
):
With respect, please do have a thorough read of my first reply in this
thread, the ensuing discussion with Darien, and Darien's proposal, in
particular this bit of it. Those
various sources explain my comment about complexity and how
Object.fromEntries
might do what you want.
-- T.J. Crowder
OK thanks for the link - can you explain where the complexity is in my proposal?
I do not use entries so I would not use Object.fromEntries
. For arrays I
could just use reduce, instead of transforming to [key, value] entries,
before factoring it into an arrayToObject
function in my code (which I
already do) when I want to do it from more than one place in my code - if
there existed no more direct way of transforming from iterable to object.
All entries arrays are iterable, but not all iterables are entries arrays,
which means that iterable is the more generic concept than entries, meaning
that iterable, not entries, is the more suitable starting point for this
functionality.
Object.fromIterable(iterable, keyFromElement[, valueFromElement])
is more
verbose than iterable.toObject(keyFromElement[, valueFromElement]) and doesn't allow chaining after iterable transformation methods (like
filter`
etc.)
An iterator is just an object with a next
method. There's no consistent
place to put any prototype methods on all iterables, so that's a nonstarter
imo.
You're thinking of iterator
It would be in the iteratable
protocol
(interface)
iterable
, excuse me
Something is iterable if it has Symbol.iterator
. Are you saying that
every iterable would then need to add a toObject
method? What happens if
it doesn't add it? What value is it if most iterables don't have toObject
but only some do?
On Wed, Aug 9, 2017 at 8:35 AM, Naveen Chawla <naveen.chwl at gmail.com> wrote:
It would be in the
iteratable
protocol
(interface)
As Jordan said, that's likely to be a nonstarter. The Iterable protocol is
very lean (exactly one required property) for a reason: So it can be
supported with minimum investment. Much better, IMHO, to put functions on
Object
and Map
(which is why that's what I suggested).
-- T.J. Crowder
The toObject
behaviour doesn't need to be "implemented" on a per-iterable
class basis. It has a constant behaviour: iterate and on each next(), pass
the value to the toKeyFromElement
and toValueFromElement
callbacks to
generate and return an object. There must be some construct by which that
can be achieved. I wouldn't call it "better" to put it on Object (for the
reasons stated), but rather a compromise in the absence of any such
construct
Java has a great example of such a construct: default interface methods
But I accept that this a very tall order for ES
JS doesn't have interfaces (yet, tho there's a proposal) and regardless, the "interface" for "iterable" is "it has Symbol.iterator, nothing more".
The only place a method like this - that produces an object - could possibly exist, is a static method on Object.
I've already outlined two existing methods to copy one object's entries to another; the only new functionality would be "creating an object from entries", hence Object.fromEntries or similar.
I still haven't seen any use cases that aren't covered by the existing "copy one object to another", or by a possible "entries to object" - does anyone have any?
Iterable to object via Object.fromIterable
It is more generic than fromEntries
I think you're misunderstanding; the function would of course take an iterable. However, an iterable of what?
If it's an iterable of objects, then what's the key and what's the value? What if it's an iterable of strings?
The only thing that makes sense is if it's an iterable that provides both a key and a value - and "entries" is the idiomatic structure in the language to respect a list of key/value pairs (besides "an object", of course).
What would you suggest?
On Wed, Aug 9, 2017 at 9:24 AM, Jordan Harband <ljharb at gmail.com> wrote:
I think you're misunderstanding; the function would of course take an iterable. However, an iterable of what?
If it's an iterable of objects, then what's the key and what's the value? What if it's an iterable of strings?
I can't speak for Naveen, he's gone off in his own direction (or rather, I have, as it was originally his thread).
But for my part, as I describe here, the common case I want to
address is that the value is the entry. No round-trip through [key, value]
pairs is required for the common use case of creating a map/object
to provide lookup by key. I certainly see why Darien may also want to
provide somthing for [key, value]
pairs, but I see it as separate.
-- T.J. Crowder
great question.
An iterable of anything!
This is the signature:
Object.fromIterable(iterable, keyFromElement[, valueFromElement])
Examples follow:
Supposing you had an array:
[
{
countryName: 'UK',
population: 65640000
},
{
countryName: 'USA',
population: 323100000
},
{
countryName: 'Denmark',
population: 5731000
}
]
...and you wanted to cache the items by country name for quick access, to get:
{
UK: {
countryName: 'UK',
population: 65640000
},
USA: {
countryName: 'USA',
population: 323100000
},
Denmark: {
countryName: 'Denmark',
population: 5731000
}
}
...you would simply do
const countriesByName = Object.fromIterable(countries, country=>country.countryName);
to get that result. (the 3rd parameter defaults to return the iterated value if not provided).
As callbacks, the keyFromElement
and valueFromElement
parameters allow
you to supply anything you like to transform from the iterated element (or
from elsewhere) into the keys and values you want:
e.g. if you had an array of strings called recentCountryNames
with ['UK', 'Denmark']
:
const recentCountryDetailsByName =
Object.fromIterable(
recentCountryNames,
countryName=>countryName,
countryName=>countriesByName[countryName]
)
would produce:
{
UK: {
countryName: 'UK',
population: 65640000
},
Denmark: {
countryName: 'Denmark',
population: 5731000
}
}
As an aside, it can easily transform entries as follows:
Object.fromIterable(entries, entry=>entry[0], entry=>entry[1])
(if the entry "keys" happen to be valid object keys, otherwise you can simply provide a different transformation for the keyFromElement
callback) but works equally well with all types of iterables
Naveen: `Object.fromIterable(recentCountryNames, countryName=>countryName,
countryName=>countriesByName[countryName])`
could also be:
Object.fromEntries(Array.from(recentCountryNames, countryName => ([countryName, countriesByName[countryName]])))
, without needing a
potentially confusing "keyCallback, entryCallback" API nor without builtins
needing to invoke user-supplied functions.
TJ: I'm confused, can you provide a code sample?
fromEntries
is much less generic than fromIterable
(since not every
iterable is an entries, but every entries is an iterable) and is much more
verbose to use in the use cases we have just discussed. I have faced such
cases often, but have NEVER faced a need for a fromEntries
. Have you?
Even if so, I've shown how fromEntries
functionality can be achieved via
fromIterable
without any additional method calls, which is not the case
vice versa
"Generic" can't apply when constructing an object, since you always need pairs of a key and a value - so it doesn't need to be generic; it needs to fit the use case.
Yes, I've had many a need for "fromEntries" or similar, which I've usually achieved with a reduce. I've never once needed to create an object from an iterable of "not entries". Needless to say, "use cases" are subjective.
new Map
accepts an iterable of entries - that's the established pattern
that any new additions would almost certainly use.
On Wed, Aug 9, 2017 at 10:56 PM, Jordan Harband <ljharb at gmail.com> wrote:
TJ: I'm confused, can you provide a code sample?
So based on the signatures I mentioned here:
Object.from(iterable, indexer, target = Object.create(null))
// and
Map.from(iterable, indexer, target = new Map)
(although byKey
or similar may be a better name), then for instance, if I
have this array in stuff.users
:
[
{id: "naveen.chowla", foreNames: "Naveen", surname: "Chowla"},
{id: "tj.crowder", foreNames: "T.J.", surname: "Crowder"},
{id: "jordan.harband", foreNames: "Jordan", surname: "Harband"}
]
then
stuff.usersById = Object.from(stuff.users, "id");
gives me a stuff.usersById
that looks like this:
{
"naveen.chowla": {id: "naveen.chowla", foreNames: "Naveen", surname:
"Chowla"},
"tj.crowder": {id: "tj.crowder", foreNames: "T.J.", surname:
"Crowder"},
"jordan.harband": {id: "jordan.harband", foreNames: "Jordan", surname:
"Harband"}
}
(Those are the same objects.)
That's the common case: A known-in-advance string key. I do that all the
time. But the indexer
parameter can also be a Symbol or a function; the
function receives the entry and returns the key, so it can be a computed
key. The test for what the indexer
is is trivially simple and fast and
need be done only once.
I can supply the target as a third argument if I want to create it myself for any reason (specific prototype, or I already have one with partial data and I'm adding more, etc):
stuff.users.push(...newUsers);
stuff.usersById = Object.from(newUsers, "id", stuff.usersById);
The Map
version does the same thing where the result (and optional third
parameter) is a Map
instead.
-- T.J. Crowder
element-property#content
By "much more generic" I mean that fromIterable
more effortlessly fits a
much wider range of use cases.
Can you give a code example of where your entries originate from? I'm
curious because you might find it easier to apply fromIterable
(since
most server side API data arrays don't come in the form of "entries").
I think a property value callback is needed for completeness (not easy to achieve the same functionality without it. Consider the examples given in the recent posts).
Keying by a string property name should, in my view, be a separate function:
Object.fromIterableByPropertyName(iterable, propertyName[,
valueFromElement[, existingObjectToUse]])
vs
Object.fromIterable(iterable, keyFromElement[, valueFromElement[,
existingObjectToUse]])
On Thu, Aug 10, 2017 at 9:49 AM, Naveen Chawla <naveen.chwl at gmail.com>
wrote:
I think a property value callback is needed for completeness (not easy to achieve the same functionality without it.
In my view, a Swiss Army knife is not a good API function. (These are
always, of course, judgement calls.) The [key, value]
case can be
addressed with Darien's Object.fromEntries
. I've literally never needed
that, but if one does, having something specific for it which fits into the
[key, value]
ecosystem makes sense to me. That's why I've said several
times now that my suggestion and Darien's draft proposal are complementary,
not conflicting/competing. In fact, it may be useful to combine them (but
not the functions they define).
Separately, I wonder how a second callback (in the case where it's a
callback) compares in terms of performance and memory churn with returning
[key, value]
arrays.
Keying by a string property name should, in my view, be a separate function
I don't see the need. If it does exactly the same thing, just using a
string directly rather than calling a function, then like String#replace
I'm happy for it to handle that. Again, though, these are always judgement
calls.
-- T.J. Crowder
I've yet to see any demonstration code use case for a fromEntries
where
it would ever be preferable over a fromIterable
, seeing as most back end
API JSON data arrays, and most data arrays in general for that matter, do
not come in the form of "entries". I would suggest that a fromIterable
with a valueFromElement
callback, rather than being a "Swiss Army knife",
addresses the fundamental building block of an object, namely keys and
values, covering e.g. the "recentCountries" use case example code a few
posts back with minimal effort.
As far as I'm concerned, "most data arrays" — at least the ones you're
speaking about — should actually be Map
s. And their iterators (from, and
to) yield key-value pairs. There is no syntax for a strict pair, so
2-element arrays have to do.
I'd still rather see some approach to generator expressions and/or iterator
functions (like map
, below) finally enter the standard somehow.
const allThePeople = [{name: "Joe", age: 24}, {name: "Barbara", age: 43},
...];
// Python generator expression
const myIndexByName = new Map([_.name, _] for _ of allThePeople);
// map function to transform iterables you can argue about the argument
order ;)
const myIndexByName = new Map(map(allThePeople, _ => [_.name, _]));
// equivalent to, but without the intermediate Array
const myIndexByName = new Map(allThePeople.map(_ => [_.name, _]));
Alex
Map.fromIterable(allThePeople, person=>person.name)
is less verbose for
your use case
On Fri, 11 Aug 2017 at 03:54 Alexander Jones <alex at weej.com> wrote:
As far as I'm concerned, "most data arrays" — at least the ones you're speaking about — should actually be
Map
s.
Why? What's the advantage?
You lose at least the square bracket and dot notations for access, as disadvantages, and I'm not aware of any advantages. If there aren't any that compensate for the disadvantages, then it's a net negative
On Thu, Aug 10, 2017 at 11:24 PM, Alexander Jones <alex at weej.com> wrote:
As far as I'm concerned, "most data arrays" — at least the ones you're speaking about — should actually be
Map
s.
I see the point you're trying to make, but
-
I routinely want paging and other access by arbitrary index
-
I routinely want to change the order, which is expensive with Maps
-
I'm usually receiving these by deserializing JSON
Granted #3 could be solved by making the JSON even more verbose than it is
([["foo", {"id":"foo",..}], ["bar",{"id": "bar", ...], ...]
) and then
creating the Map from that temporary array of arrays but...no. :-) Not least
because of the knock-on effects on the generating code. So I'd need a way
to create the Map from the array even if I were using Maps from then on
(such as my Map.from
).
Arrays are a great random-access, sortable, tranferrable format; and highly-optimized on modern JavaScript engines. No need to change it. And if I did change it, I'd still need a way to create the Map in the first place from the obvious transfer format: An array.
-- T.J. Crowder
@Alexander The idea of a generalized map
function (etc, I’m guessing) is
appealing. From the way you talked about it, it sounds like there may have
been
past discussion on the topic. Are there any proposals for this or major
ideas
being batted around?
Why? What's the advantage? You lose at least the square bracket and dot notations for access, as disadvantages, and I'm not aware of any advantages. If there aren't any that compensate for the disadvantages, then it's a net negative
@Naveen — a handful of things. Objects are indeed a perfectly reasonable
choice
for modeling kvp collections a lot of the time. On the other hand, because
objects in ES serve double duty as ways to model data and ways to "model
code",
they are not always ideal for the former case on account of the features
that
exist mainly to serve the second. Some examples of things that make maps
useful:
inherently iterable; no prototype chain access lookup; no collision with
Object.prototype
property name when hashing by arbitrary keys; potentially
more efficient for situations where keys are frequently removed;
map.has()
is
more straightforward than having to consider whether you want key in
or
Object.hasOwnProperty
or which properties have been defined as enumerable
or
not; iteration order of entries is 1:1 with insertion order; and of course,
keys
can be of any type. Further, maps can be subclassed to constrain the types
that
they may hold or add other behaviors without needing to define custom
Proxies.
Some general use cases: registries; hashing data where keys are from external input; kvp collections which are ordered; kvp collections which will be subject to later transformations; kvp collections to which new keys are frequently added or removed.
While I hope that information is somewhat helpful, there are probably much more detailed resources online (including, I suspect, past discussions on this list) which could explain some of those things better or which include cases I haven’t thought of.
Of these, insertion ordering is the only one that may be compelling enough
to me when I require that, to overcome the disadvantages. I never use the
object prototype and I'm not convinced about the performance aspect. I
reiterate though that Map.fromIterable(allThePeople, person=>person.name)
is less verbose for the stated use case
Map.fromIterable
takes an iterable of values, and a key function. Would a
Map.prototype.toIterable
return only the values - that's already
Map.prototype.values
? It feels like there is a symmetry issue here.
Perhaps this could be Map.fromValues
?
Worth also remembering that compressing every possible use case down to an
absolute minimum has a cost if the resultant language has too many features
like this. I think new Map(...kvps)
is a general solution that is
actually good enough for "indexing" purposes, and means that people only
have to internalise one method of construction via iterables.
That said, a helper function to allow constructing map-like objects from arbitrary iterables would maybe be a bit more composable?
// This works with Map, WeakMap, Immutable.Map, etc.
function* keyedBy(iterable, keyFn) {
for (const element of iterable) {
yield [keyFn(element), element];
}
}
const allThePeople = [{name: "Joe", age: 24}, {name: "Barbara", age: 43},
...];
const index1 =
new Map(keyedBy(allThePeople, _ => _.name));
// c.f.
const index2 =
Map.fromValues(allThePeople, _ => _.name);
My proposal was Map.fromIterable(iterable, keyFromElement[, valueFromElement[, existingMap]])
so it's not meant to be symmetrical with
values
anyway. It was as an equivalent of Object.fromIterable(iterable, keyFromElement[, valueFromElement[, existingObjectToUse]])
as a means to
construct an object's keys and values from any iterable.
I also was just thinking that both can perfectly coexist with
Array.prototype.toObject(keyFromElement[, valueFromElement])
which has
the advantage of chain-ability after array transformation methods (like
filter
etc.). Array is such a fundamental construct that I would find
myself using this one the most frequently
Why not embrace Array.prototype.reduce
instead of trying to abstract it
away?
const identity = a => a
const toObject = (fk, fv = identity) =>
(acc, curr) => (acc[fk(curr)] = fv(curr), acc)
const arr = [['a', '1'], ['b', '2'], ['c', '3']]
arr.map(a => [a[0], parseInt(a[1], 10)])
.filter(a => a[0] !== 'c')
.reduce(toObject(a => a[0]), {}) // { a: 1, b: 2 }
reduce(toObject)
clearly communicates intent, and you can even provide an
existing object for merging.
Verbosity.
It's a task common across many project types and involves only 2 fundamental types in ES: array and object
Compare:
const cache = array.reduce((cache, element)=>{cache[element.id] = element;
return cache;}, {});
with
const cache = array.toObject(element=>element.id);
since the signature would offer additional optional valueFromElement
and
startingObject
parameters nobody loses anything.
For the same reason why we have filter, forEach, map etc. Reduce is actually rather low level primitive (if we can call functional concept low level) and specialised methods should be preferred over it's usage if possible.
Honestly, promoting the use of Object for this, and coupling the solution to Array, feels like the wrong direction for the language to me personally. By definition, such a map constructed from a set of homogenous values, for indexing purposes, has a clear key and value type. This guidance from MDN seems to be the right message we should be sending to developers:
developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map
... ask yourself the following questions:
- Are keys usually unknown until run time? Do you need to look them up
dynamically?
- Do all values have the same type? Can they be used interchangeably?
- Do you need keys that aren't strings?
- Are key-value pairs frequently added or removed?
- Do you have an arbitrary (easily changing) number of key-value pairs?
- Is the collection iterated?
If you answered 'yes' to any of those questions, that is a sign that you
might want to use a Map. Contrariwise, if you have a fixed number of keys, operate on them individually, or distinguish between their usage, then you probably want to use an Object.
Object self-promotes because it is nearly always a perfect fit for this:
great performance, great literal syntax, concise access syntax. The only
compelling case to use a map instead is when insertion ordering is
required, despite the MDN article. I wouldn't oppose an equivalent
Array.prototype.toMap
and Map.fromIterable
, I would just hardly ever
use it
is nearly always a perfect fit for this
The fact that it is so close to being useful, but has silly surprises like
'toString' in everyObject
, actually gives JS a bad reputation and
contributes towards people being driven to other languages that have
cleaner approaches to data types.
For iteration, Object.keys()
doesn't include any property in the
prototype chain like "toString" etc.
I've never been concerned about needing to allow "toString" etc. as valid
cache keys, so it's never been an issue for me so far. However, if I did in
future, I would honestly look to delete them from the Object.prototype in
my application since I never use anything in there, as long as I knew it
was safe to do so. If and only if not, then yes I might consider using a
map for those cases. But just for consistency with my other caches I might
just instead consider appending something to each object key in factored
out access methods (or just at source data) for this cache so there can't
be overlap
On Mon, Aug 14, 2017 at 7:33 AM, Naveen Chawla <naveen.chwl at gmail.com> wrote:
I've never been concerned about needing to allow "toString" etc. as valid cache keys, so it's never been an issue for me so far. However, if I did in future, I would honestly look to delete them from the Object.prototype in my application since I never use anything in there, as long as I knew it was safe to do so.
I very much doubt it's safe; if you're using any library, I expect it would blow up fairly quickly.
It's also not remotely necessary. Just create an object with no prototype:
Object.create(null)
.
-- T.J. Crowder
OK, I have written up a proposal that I really hope satisfies every single requirement outlined in this discussion. If not, please recommend amendments. It is here: TheNavigateur/arrays-and-other-iterables-to-objects-and-maps . Do give comments here either way
As far as I remember Maps have the same performance as objects in case of dynamic property access for small collections and better for big ones (I have value of 255 in my mind, but I can't test it now). And objects do maintain insert order, at least in some cases.
I've often needed to cache array elements by a unique key each element has, for quick access.
This is a shortcut:
const elementsById = elements.toObjectByProperty(element=>element.id);
Support?