[HN Gopher] TC39: Add Object.groupBy and Map.groupBy
___________________________________________________________________
TC39: Add Object.groupBy and Map.groupBy
Author : moritzwarhier
Score : 52 points
Date : 2023-12-19 21:20 UTC (1 hours ago)
(HTM) web link (github.com)
(TXT) w3m dump (github.com)
| carterschonwald wrote:
| This seems like a nice addition. Is it soemthing that a lot of
| tools roll their own, or is something that can't be done in user
| space? I'm relatively ignorant of the limits of js
| tibbar wrote:
| It's fairly easy to roll your own. An example implementation of
| groupBy in lodash, for example: https://github.com/lodash/lodas
| h/blob/4.17.15/lodash.js#L939....
|
| More broadly, this is an example of a utility function for
| organizing data in a data structure, which JavaScript and all
| other major programming languages are well-equipped to
| implement in arbitrary ways, so this is just a convenience
| function. It's not like a hook into underlying environment APIs
| that can't be independently shimmed.
| jauntywundrkind wrote:
| Trying to read lodash can (to put it mildly) be difficult.
| Lots of code reuse.
|
| This is probably missing checking for a bunch of corner
| cases, but this is the general idea: // quick
| & dirty Object.groupBy function groupBy(iterable,
| callbackFn) { const out = {} for(const [key,
| value] in Object.entries(iterable)) {
| out[callbackFn(value, key)] = value } return
| out }
|
| I literally wrote something very like this yesterday (after
| checking & finding it only got added to Node 21, and deciding
| not to pull in a dependency).
| tibbar wrote:
| I think `out` should be typed as a map to an array in this
| case; each iteration should be appending to the
| corresponding array instead of overwriting the entire
| entry.
| quonn wrote:
| That's closer to keyBy than groupBy.
| moritzwarhier wrote:
| Oh yes, I agree.
|
| I remember me running into the problem of internal
| dependencies when extracting parts of it into a non-
| ESM/non-build project some years ago...
|
| Like, the internal consistency of the library probably
| benefits, and with tree-shaking it works reasonably well
| but it is still a poster example of DRY vs KISS...
|
| It has its benefits, but its API surface can be annoying,
| too. When I encounter it, I often have to think for a
| second and look at the docs, e.g. some cryptic xor or map
| function with three parameters, one of which being an
| optional config object.
|
| OTOH, it can be great to have a well-tested implementation
| at hand for all kinds of higher-order functions, like
| throttle, debounce etc
| explaininjs wrote:
| Aaannd... now you have a prototype pollution vulnerability
| :)
|
| Object.from(null), always.
| ricardobeat wrote:
| It's very funny to point to lodash as an example of
| simplicity. Here is what happens when you start unraveling
| the code you linked to:
|
| https://gist.github.com/ricardobeat/040af80971273f5abd71c2bf.
| ..
|
| I got tired, but wouldn't be surprised if the whole thing
| goes beyond 1000 lines. It's basically a black hole. Better
| hope you really, really trust their test suite, it's
| impossible to debug and there must only be a handful of
| people familiar with the whole codebase.
| erulabs wrote:
| It can be trivially rolled by hand, but it's also a pretty
| common function in other languages. I'd guess most node
| programmers reach for lodash for this.
| paulddraper wrote:
| > a pretty common function in other languages
|
| Except oddities like C, C++, Perl, PHP, and Go :)
| erulabs wrote:
| Oh PHP programmers would just rely on MySQL's groupBy, we
| both know it! :P
| paavohtl wrote:
| It can absolutely be written in JavaScript (it is a Turing
| complete language after all), and it is a common utility
| function found in most general utility libraries like Lodash
| and Ramda.
| moritzwarhier wrote:
| It is indeed something that you often roll your own, write an
| utility function for, or include libraries like lodash for.
|
| JS standard library is notoriously deficient.
|
| This is a good addition in my view too, especially including
| Map.
|
| When dealing with DB/API results, one can of course always say
| that such grouping in the client is an anti-pattern.
|
| But in reality it can be reasonable and useful.
|
| If you write one-off algorithms in JS to transform small
| amounts of data on the client, you probably habe done this.
|
| And even for data-heavy applications, it could prove to be a
| performance benefit for functions that use Map instances during
| computation.
|
| Though that's just wishful thinking until the runtime
| developers choose to optimize these functions, if possible.
| pmontra wrote:
| > When dealing with DB/API results, one can of course always
| say that such grouping in the client is an anti-pattern.
|
| It depends on how many data you're working on. This groupBy
| is useful for filters in data tables.
|
| If you have to download a zillion of records and filter them,
| client side filtering is a bad idea. If you have a few
| thousands of them, it could possibly be the faster solution
| overall.
| moritzwarhier wrote:
| Exactly.
| thenbe wrote:
| You can roll your own or use a utility library. A simple zero-
| dependency library would be something like just-* [1]. Although
| I now prefer remeda [2] as it seems to have the best typescript
| support, especially the strict variants such as
| `grouBy.strict`.
|
| [1] https://github.com/angus-c/just#just-group-by
|
| [2] https://remedajs.com/docs#groupBy
| jackconsidine wrote:
| Anyone know if `keyBy` will also be supported? I suppose it'd be
| pretty trivial to take a `groupBy` and make it `keyBy`, but then
| again it's pretty trivial to implement `groupBy` from scratch.
|
| I no longer install lodash that often anymore
| derefr wrote:
| I'm a big fan of the design of Elixir's Enum.group_by, which
| has one-parameter and two-parameter forms. The one-parameter
| form is like any other language's groupBy -- but the two-
| parameter form takes two mapping closures; passes each element
| into both of them; and uses the output of the first closure as
| the grouping key, and the output of the second closure as the
| value to be registered under that grouping key.
|
| This flexible primitive enables you to do basically any
| grouping transform you want (incl. the Lodash-style keyBy) in a
| single short line of code.
|
| It's a lot like a sortBy operation, in that to emulate it, you
| would have to do a map to extract the key, producing pairs;
| sort (or in this case group) the pairs; and then deep-transform
| the pairs inside the data structure by unwrapping the keys off
| them. In other words, it's something that's a bit too high-
| friction to reach for if the language doesn't just give it to
| you (you'd probably do what you're planning to do some other
| way); but if the language _does_ give it to you, you 'll use it
| quite often.
| edflsafoiewq wrote:
| For reference, keyBy returns an object with the same keys as
| groupBy, but the value of a key is the last element to produce
| that key, instead of an array of all of the elements that did.
| bakkoting wrote:
| No current plans for `keyBy`, and I don't know that it's really
| that well-motivated. (I am on TC39.)
| moritzwarhier wrote:
| You can do that by nesting Array.prototype.map
|
| in the parameter for Object.fromEntries
|
| in an easy way (probably not as optimized as a built-in, but
| that might be irrelevant for most cases, since its just a
| duplicated iteration, not quadratic)
| koito17 wrote:
| Nice, the JS standard library is gradually getting functions I
| take for granted in ClojureScript. :)
|
| I noticed this part in the proposal groupBy calls
| callbackfn once for each element in items, in ascending order,
| and constructs a new Object of arrays. Each value returned by
| callbackfn is coerced to a property key
|
| Does JS allow arbitrary objects as keys? I am asking because
| `group-by` in ClojureScript is quite flexible. e.g. you can find
| anagrams like so (def words ["meat" "mat" "team"
| "mate" "eat" "tea"]) (group-by set words) ; set is a
| function that creates sets from collections ;; =>
| {#{\a \e \m \t} ["meat" "team" "mate"] ;; #{\a \m \t}
| ["mat"] ;; #{\a \e \t} ["eat" "tea"]}
|
| I am wondering how one could translate this using Object.groupBy
| as specified in the proposal.
| tibbar wrote:
| JS does indeed allow mapping arbitrary objects to keys.
| However, it is the memory address of the object that is hashed,
| not the semantic value. So, for example, if you do:
| objects = {} items1 = ["a", "b"] items2 = ["a",
| "b"] objects[items1] = 1 objects[items2] = 2
|
| then objects[items1] will return 1, and objects[items2] will
| return 2, but objects[items1] !== objects[items2].
|
| EDIT: Sorry, this only works for dictionaries that are Maps,
| not Objects! See the responses. My fuzzing about in the console
| led me astray; you should start with `objects = new Map()`
| instead of `objects = {}`.
| thegeomaster wrote:
| This is not true. I think you're confusing JS objects with
| Maps. This will just coalesce the key to string and overwrite
| one element of the object with the other.
|
| With a map it works like you described:
| objects = new Map() items1 = ["a", "b"]
| items2 = ["a", "b"] objects.set(items1, 1)
| objects.set(items2, 2)
| mediumdeviation wrote:
| Actually that only works with Map. For plain objects the key
| is always the stringified cast of the key.
| > o = { [{}]: 1 } { '[object Object]': 1 } >
| k = { toString() { return 'a' } } { toString:
| [Function: toString] } > o = { [k]: 1 } { a:
| 1 }
| aragonite wrote:
| Right ... and it seems it's not even possible to simulate
| object keys using Proxy traps: new Proxy(
| {}, { get(target, property) {
| return typeof property }, }, )[{}]
| === 'string'
| koito17 wrote:
| Ah, I didn't know Map in JavaScript allowed arbitrary keys
| whereas Object always serializes to strings. I guess that is
| the reason for having a Map.groupBy in the proposal.
|
| Thanks for taking the time to explain, everybody
| moritzwarhier wrote:
| JS allows arbitrary values (including objects/references) as
| keys in Maps, but not in objects, there it is cast to string by
| default (e.g. "object [Object]", "null" or "undefined", this is
| not intended usage of course).
|
| The symbol primitive is also allowed as an object key, using
| square bracket access.
|
| And arrays are "exotic objects" that have some special
| behaviors around their keys (auto-updating length property)
| ForkMeOnTinder wrote:
| > Does JS allow arbitrary objects as keys?
|
| Object doesn't, but Map does. It's generally a good idea to use
| Map anyway for dynamic keys.
| freedomben wrote:
| You ClojureScript people seem to be stalking me :-D
|
| I've wanted to learn Clojure for years but haven't found the
| right reason, until discovering Logseq. Such a cool language!
| Waterluvian wrote:
| When it comes to helper functions on existing types, I feel like
| you really can't have too many... within reason.
|
| If we added like four new kinds of object/map, that complicates
| things... but just formalizing more common access and iteration
| patterns for existing structures seems low risk.
|
| What I can't wait for are all the set operations that would make
| 'Set' truly powerful.
| mbStavola wrote:
| This is one of those functions I end up implementing in pretty
| much every single non-trivial JS project I work on. Glad to see
| some movement here.
| pqdbr wrote:
| On a side note, I really wish I could learn to tell exactly if a
| new feature like this is already supported in a project I'm
| working.
|
| babel.config.js is just a mistery to me. @babel/present-env,
| targets: { node: 'current' }, forceAllTransforms, useBuiltIns,
| 'corejs', modules, @babel/plugin-proposal-object-rest-spread.
|
| I mean, I generally dump it into Chat GPT and ask for an
| explainer, but... how can I know for sure I can use
| `array.reverse()` and it will be correctly handled in older
| browsers?
|
| And how does the babel version in yarn.lock relate to all this?
|
| What about the .browserslistrc which contains 'defaults' ?
|
| My god.
| Vt71fcAqt7 wrote:
| I hope they stick to these useful helper functions instead of
| adding more complexity like Type Annotations that provide no type
| soundness or runtime checks of any kind[0]
|
| [0] https://github.com/tc39/proposal-type-annotations
| wffurr wrote:
| That's an interesting take when the stated purpose of that
| proposal is "to enable developers to run programs written in
| TypeScript, Flow, and other static typing supersets of
| JavaScript without any need for transpilation".
|
| That seems like a fine goal. Allow runtimes to execute JS with
| type annotations as-is.
| Vt71fcAqt7 wrote:
| The issue for me is four-fold:
|
| 1. Adds complexity to the language
|
| 2. Adds complexity to engines
|
| 3. Adds complexity to developers, especially new developers
| ("wait is it typed or not")
|
| 4. Most importantly, all but guaranties we will never have
| true types in JavaScript for things that could benefit from
| it like node or electron where instant compile time isn't
| necessary.
|
| All this for a feature only helping some developers some of
| the time so they can run code that will be stripped out in
| production to reduce size anyway on their local browser a bit
| more easily.
| aragonite wrote:
| What's the thinking behind attaching Object.groupBy to the Object
| constructor? With the other Object.X() functions I can easily see
| why it makes sense they exist on the Object constructor, because
| they generally follow the pattern of taking an object as
| argument, and say, freezes _that object_ , returns _that object_
| 's [[Prototype]], returns _that object_ 's keys, check if _that
| object_ is sealed, etc. By contrast, Object.groupBy is a utility
| function that takes an iterable and a callback, and doesn 't seem
| to have anything distinctively to do with "Object" per se.
|
| (I guess one exception is Object.is, which takes two _values_ as
| arguments. But even Object.is makes a lot of sense existing on
| the Object constructor because it exposes an important abstract
| operation (SameValue). Object.groupBy is only a utility function)
| danvk wrote:
| What about Object.fromEntries?
|
| See https://github.com/tc39/proposal-array-grouping for why
| this isn't a method on Array.prototype.
| flqn wrote:
| Constructs an object from kv pairs, definitely assoctiated
| with the "Object" prototype. Groupby is more dubious
| LegionMammal978 wrote:
| Map.groupBy() returns a _Map_ with the keys mapped to array
| values. In parallel, Object.groupBy() returns an _Object_ with
| the keys cast into property names. Thus, I 'd say it fits the
| pattern of Object-related static methods, since it's using the
| Object mechanism as a map.
| bakkoting wrote:
| We originally tried to put it on Array.prototype, under
| multiple different names, and that broke various pages in
| various ways. So we gave up and had to put it somewhere else.
| And it's conceptually similar to `fromEntries` - they're both
| ways of making an object. So the Object constructor was the
| obvious choice.
| replygirl wrote:
| it's a noble approach, but feels like it's at its limits if
| no one could find a reasonable name that works with Array.
| there's gotta be a point at which it makes sense to EOL
| third-party stuff like that: set a date far in the future,
| and any pages where document.lastModified is greater get
| Array.groupBy as well
| bakkoting wrote:
| Not gonna happen: https://github.com/tc39/faq?tab=readme-
| ov-file#why-dont-we-j...
|
| Browsers aren't going to break old pages.
| no_wizard wrote:
| This is why we need a global Iterator / Iterable type. These
| are iterators at the end of the day after all. That would
| also signal the fact they could be used for more than one
| data structure (Array, Map, POJOs)
| bakkoting wrote:
| Global iterator type is coming:
| https://github.com/tc39/proposal-iterator-helpers
|
| But a method named `groupBy` on iterators traditionally
| means a different thing: https://github.com/tc39/proposal-
| array-grouping/issues/51#is...
|
| Global iterable type it's too late for, since there's many
| extant iterables in the language and on the web which don't
| have it in their prototype chain and can't reasonably be
| changed.
| dagurp wrote:
| I would like to have .partition as well but I guess I can get by
| with groupBy.
|
| Array.product would be sweet as well (i.e. like product in
| python's itertools).
| mattlondon wrote:
| In case you need a example, there is a good one here:
| https://github.com/tc39/proposal-array-grouping/
| javajosh wrote:
| I don't think they should do this especially since they don't
| even have an Array.prototype.peek()
___________________________________________________________________
(page generated 2023-12-19 23:00 UTC)