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