tc39 / proposal-object-pick-or-omit

Ergonomic Dynamic Object Restructuring.
MIT License
94 stars 5 forks source link

Can we go ahead and add a pick/omit combined function to this proposal? #13

Open tolmasky opened 1 year ago

tolmasky commented 1 year ago

Oftentimes when I use these operations, I use them in conjunction, so instead of having to iterate over the same object twice, can we have something like partition added as a third method here:

const [picked, omitted] = partition(object, predicate);

pick and omit could be defined in terms of partition, or vice versa

ljharb commented 1 year ago

Can you elaborate on the use cases you have here?

tolmasky commented 1 year ago

I think partition is a fairly common function that "needs no introduction":

  1. From underscore: https://underscorejs.org/#partition
  2. From Ramda: https://ramdajs.com/docs/#partition

The reason to mention it in this proposal is precisely because it is so intimately related to these two functions (again, it is either the composition, or arguably the ancestor of them).

Perhaps more importantly though, you can kind of think of JavaScript as already supporting pick, omit, and partition in the "statically known keys" case through destructuring:

// To "omit" `a`, `b`, and `c`, notice we do still get to "keep" a, b, and c without further work:
const { a, b, c,         ...objectWithOmissions } = someObject;
            ^ "omitted"  |
                         | "picked"

// Alternatively, if you wish "pick" a, b, and c, you can do the same thing:
const { a, b, c,         ...omittedItems } = someObject;
            ^ "picked"   |
                         | "omitted"

The key to pick and omit of course, beyond being syntactically clearer what you are trying to do, is that they uniquely allow you to handle case where the desired picked and/or omitted keys are determined at dynamically. It should follow that the use cases of using ...rest with normal keyed destructuring should serve as motivating cases for `partition. It would be unfortunate to have to lost the "rest feature" when your keys go from statically known to only dynamically known.

ljharb commented 1 year ago

partitioning an array does, but partitioning an object isn't something i'm familiar with.

In general, the pushback this proposal got from committee was against dynamic functions - iow, the preference was static syntax forms, not dynamic function APIs.

tolmasky commented 1 year ago

partitioning an array does, but partitioning an object isn't something i'm familiar with.

I mean, they're all just operating on objects, right ;)? We just currently only have a pick function that works on array indexes instead of and property keys called filter. There's really no reason that "keyed" collections would suddenly not be interested in pick/rest or omit/rest vs. indexed collections, especially if we once again realize we do offer this exact feature in the static form with destructuring.

In general, the pushback this proposal got from committee was against dynamic functions - iow, the preference was static syntax forms, not dynamic function APIs.

Sure, but this makes this proposal no more or less dynamic, it just provides a convenience method, and performance improvement, for having to use both of these, which I at least run into all the time. In the package.json case, if you start having multiple keys with "dev" prefixes, it would be quite useful to do:

const [devConfiguration, configuration] = partition(packageJSON, key => key.startsWith("dev"));

doStuff(configuration);

if (dev)
    doStuff(devConfiguration)

Separately, as a general defense of this proposal, with respect to the pushback for "dynamicism" vs. syntanctic forms, this feels like saying we shouldn't have filter because we can destructure to "pick" the statically known indexes you want. For example, just do [,,,x] = my_array and you're on your own if you want pick based on anything other than a specific key (in this case the keys are indexes).

ljharb commented 1 year ago

Conceptually there's a huge difference between an array and a non-array object.

tolmasky commented 1 year ago

Conceptually there's a huge difference between an array and a non-array object.

The problem is that in JavaScript, objects do double duty as "Record" and "HashMap". There's perhaps a huge difference if we consider them records, but I think we have to also consider the very very common HashMap use case, especially considering we won't be getting Map in JSON ever. And in that aspect, they are more conceptually similar than different. You'll find many (most?) other language agree by representing this either by placing them both under some "Collection" subclass that would have all these methods, or placing them on a Collection interface. Either way, it is usually expected that most filtering/mapping/etc. operations exist on all collections, instead of waiting for specific motivating examples in each instance. The idea is precisely the realization that if something is useful in one keyed collection, there's a good change it will be useful on other types as well. For an example of this in JS, the immutablejs library handles partition in precisely this way by placing it on the Collection parent class: https://immutable-js.com/docs/v4.3.0/Collection/#partition()

theScottyJam commented 1 year ago

filtering/mapping/etc

Yeah, I wish those existed for objects as well, but they don't. The API available for dealing with objects is quite small, and it seems that's intentional. When an Object.map() proposal was presented, they were concerned about the "slippery slope" effect it would have in causing all sorts of other array-like methods to be requested to be available for objects, so the proposal tried to be morphed to be more generalized, then it was ultimately dropped.

aleen42 commented 1 year ago

To put it simply, there are two problems we discuss here:

  1. Should the proposal provide a method like _.groupBy which can use as a reducer to group values (array) or items (items) in two or more dimensions, not only for the particular case when we want to pick or omit something.

    The proposal should focus on the operational process, rather than the result of how it constructs.

  2. Do you think we should provide a proposal for iterating different objects? I think Object.entries() and [Iteration protocals] are sufficient for us to implement a general iteration above arrays and objects like the following pseudo-code:
    function iterate(collection, callback) {
        if (isArray(collection) || collection instanceof Map) {
            // use `Array.prototype.forEach` or `Map.prototype.forEach`
            collection.forEach(callback);
        } else if (nonNull(collection)) {
            // iterate with iteration protocol (for..of)
            const simple = isIterable(collection);
            // noinspection JSCheckFunctionSignatures
            const iterator = (simple ? collection : Object.entries(collection))[Symbol.iterator]();
            if (simple) { // iterate for collection
                for (let index = 0; ; index++) {
                    const next = iterator.next();
                    if (next.done) break;
                    callback(next.value, index, collection);
                }
            } else { // iterate for Object entries
                for (; ;) {
                    const next = iterator.next();
                    if (next.done) break;
                    callback(next.value[1]/* entry.value */, next.value[0]/* entry.key */, collection);
                }
            }
        }
    }