Skip to content
This repository has been archived by the owner on Jan 26, 2022. It is now read-only.

.fromEntries vs .from #7

Closed
zloirock opened this issue Mar 8, 2018 · 27 comments
Closed

.fromEntries vs .from #7

zloirock opened this issue Mar 8, 2018 · 27 comments

Comments

@zloirock
Copy link

zloirock commented Mar 8, 2018

I haven't any strict opinion about it, but already available proposal .from / .of methods on collections. On Map / WeakMap, it works with entries.

@ljharb
Copy link
Member

ljharb commented Mar 8, 2018

Has that proposal been presented to the committee yet?

@bathos
Copy link
Collaborator

bathos commented Mar 8, 2018

I wrote about the name question a bit here:

https://github.com/bathos/object-from-entries#method-name

The name could as easily be from, which is consistent with Array.from. The
only downsides might be:

  • it’s perhaps less clear; it’s comparatively more obvious that Array.from
    would accept an iterable
  • doesn’t signal its symmetry with Object.entries
  • could be inconsistent if future methods allow building objects 'from' other
    sources

I don’t have a strong opinion on this. I thought 'fromEntries' seemed more intuitive in light of Object.entries. If it’s likely "from" will end up being a method on all the iterable constructors, then perhaps that consistency would trump the value of communicating the contract in the name.

@bathos
Copy link
Collaborator

bathos commented Mar 8, 2018

I should also add that I haven’t done any research on whether there’s non-clobbering Object.from or Object.fromEntries usage in the wild. It seems pretty unlikely given it’s static. I guess there’s always Object.smoosh :)

@zloirock
Copy link
Author

zloirock commented Mar 8, 2018

@ljharb stage 1.

@zloirock
Copy link
Author

zloirock commented Mar 8, 2018

I think that it should be sync. cc @leobalter @allenwb

@bakkot
Copy link
Collaborator

bakkot commented Mar 10, 2018

I could go either way on this question. On the one hand, all of Map, Set, Array, and Object have prototype entries methods and would per that proposal have from, so it would be weird that only Object would have fromEntries instead of from. On the other, Object really isn't the same kind of container as the rest of those, what with accessors and prototypes and so on, and its constructor can't be made to accept a list of entries the way, for example, Map's does.

My tentative vote is to stick with fromEntries, but could be persuaded.

@WebReflection
Copy link

FWIW I think fromEntries is OKish but I also see the elephant in the room: the absence of an abstract entries definition.

If we had that, like we have a proper definition of an iterator, we could consider exact same modern approach through a generic Symbol.entries approach.

Entries

An iterable collection of key/value pairs

Pair

An array of length 2 with any key at index 0 and any value at index 1.

Symbol.entries

A method that returns entries. Same as we recently put Symbol.iterator in the Array.prototype and others, we could put Symbol.entries in the Object.prototype.

Object.defineProperty(
  Object.prototype,
  Symbol.entries,
  {
    configurable: true,
    value() {
      return Reflect.ownKeys(this)
        .filter(k => this.propertyIsEnumerable(k))
        .map(k => [k, this[k]]);
    }
  }
);

Object.from(generic)

A method that checks generic type and does exactly what fromEntries does in case the generic has a Symbol.entries defined somewhere.

Object.defineProperty(
  Object,
  'from',
  {
    configurable: true,
    value(generic) {
      if (Symbol.entries in generic) {
        return generic[Symbol.entries]()
          .reduce((o, e) => ((o[e[0]] = e[1]), o), {});
      } else {
        return {};
      }
    }
  }
);

Applications

Whenever a class has Symbol.entries it could be compatible with Object.from.

In case of Map, as example, Map.prototype[Symbol.entries] = Map.prototype.entries, and so on for others with entries methods.

Rationale

Using a special Symbol instead of checking for methods is the modern/more elegant way to go and explicitly define a capability.

My 2 cents.

@bathos
Copy link
Collaborator

bathos commented Mar 13, 2018

@WebReflection I don’t understand the examples given —

and so on for others with entries methods.

According to that logic, while Object.fromEntries(mapInstance) would work, the following would be quite a surprise:

Object.fromEntries(Object.entries({ a: true, b: false }));
// gives us { '0': [ 'a', true ], '1': [ 'b', false ] }

It doesn’t seem to solve the same set of problems as the proposed behavior.

@ljharb
Copy link
Member

ljharb commented Mar 13, 2018

@WebReflection interesting idea; we could modify it a bit by not installing a default implementation on Object.prototype, but instead using that implementation when Symbol.entries wasn't present.

One catch would be that Map/Set/Array .entries() returns an iterator; so either your default implementation might need a bit of tweaking (to return an iterator), or, the Symbol.entries methods wouldn't be able to just be === to existing .entries methods.

@bathos I think the suggestion is that you wouldn't call Object.entries at all - you'd just do Object.from({ a: true, b: false }).

The downside here, though, is that the common case is when you want to transform one list of entries to another - which would be relatively awkward with @WebReflection's suggestion.

@bathos
Copy link
Collaborator

bathos commented Mar 13, 2018

Right. I’m not clear on what the utility would be — @WebReflection can you explain more about what you see the use cases as? Does it address the common case and I’m not spotting it?

@WebReflection
Copy link

@bathos I've overly simplified with the generic notion that if there's entries then it's OK to use it 'cause in the Array case it won't be OK but the Symbol.entries there could do the right thing.

Example, if the returned array Object.entries({ a: true, b: false }) had Symbol.entries in its chain, the proposed Object.from(Object.entries({ a: true, b: false })) would pass through .reduce((o, e) => ((o[e[0]] = e[1]), o), {}); hence Object.from(Object.entries({ a: true, b: false })) would be exactly the same as Object.fromEntrie(Object.entries({ a: true, b: false })).

The proposal is that Object.from([['a', 1], ['b', 2]]) with a Symbol.entries in the Array.prototype that returns itself produces already {a: 1, b: 2}.

The Symbol.entries, once invoked, should return entries. In the Map case it's obvious to spot, in the Object.entries(...) case it's obvious to spot, in the Array case it's less obvious because an Array is already valid candidate as entries.

@ljharb

you'd just do Object.from({ a: true, b: false }).

that would use Object.prototype[Symbol.entries] that would return Object.entries({ a: true, b: false }) which is valid, being an entry (or an array with a Symbol.entries that returns itself) so both cases would work.

when you want to transform one list of entries to another - which would be relatively awkward

I've forgotten about it, here it is, same as Array.from

Object.defineProperty(
  Object,
  'from',
  {
    configurable: true,
    value(generic, transform = (v, k) => v) {
      if (Symbol.entries in generic) {
        return generic[Symbol.entries]()
          .reduce((o, e) => (o[e[0]] = transform(e[1], e[0]), o), {});
      } else {
        return {};
      }
    }
  }
);

@WebReflection
Copy link

WebReflection commented Mar 13, 2018

P.S. like I've said, if we had an Entries definition in the language anything that returns Entries would be already a good candidate and maybe the Symbol.entries wouldn't be necessary.

However, when it comes to composition, Symbol technique already showed it plays very well ... so ... maybe

Object.defineProperty(
  Array.prototype,
  Symbol.entries,
  {
    configurable: true,
    value() {
      for (const pair of this) {
        if (!Array.isArray(pair) || pair.length !== 2)
          throw new TypeError('invalid pair');
      }
      return this;
    }
  }
);

That special method would make every returned entries a valid candidate and its check will ensure no issues

edit

as counter idea there could be this specialised class and an instance Entries check in Object.from:

class Entries extends Array {
  [Symbol.iterator]() {
    const out = [];
    for (const pair of this) {
      if (!Array.isArray(pair) || pair.length !== 2)
        throw new TypeError('invalid pair');
      else
        out.push(pair);
    }
    return out;
  }
}

That would be used as return value of Object.entries and friends ... but also it would play less nicely in terms of composition.

@bathos
Copy link
Collaborator

bathos commented Mar 13, 2018

I appreciate the concept and wiser folks may see good reasons to take the well-known symbol approach. My sense personally is that it seems perhaps overbaked — the complexity and reach doesn’t seem justified by the problem space. Perhaps I’m defining the problem space too narrowly though, not sure.

Regarding a definition for Entries, it may be relevant that the existing consumer of entries, Map, uses a contract which is "an iterable of objects". It accesses the "0" and "1" properties of each object, so you could further say "an iterable of objects on which "0" and "1" properties may be accessed", though it does not care whether they have been defined.

If the definition were formalized to be "an array of length 2 with any key at index 0 and any value at index 1", Map would become a sort of exception in terms of its permissiveness. I don’t know if this would matter or not.

@WebReflection
Copy link

WebReflection commented Mar 13, 2018

@bathos while I understand your point of seeing it as overbaked, I find fromEntries idea underbacked and non easily composable/extensible.

Imagine a DOM node with a state represented by its dataset, and imagine dataset can implement a Symbol.entries so that Object.from(node.dataset) can snapshot that state instead of holding a live proxy object.

Imagine a Blob that could expose, though Symbol.entries, as well as an image or even a Request.

I see Object.from as an opportunity for the language to dumb down complex objects, and not only to recreate an object from a list of pairs.

Symbol.iterator did a good job for everything Array (iterator) based, who knows how many new cool things we can do with Symbol.entries.

Blocking it at fromEntries only ? Yeah, maybe it's good enough for one use case, but it brings nothing more than what a .reduce(...) could bring already.

@bathos
Copy link
Collaborator

bathos commented Mar 13, 2018

I should clarify that

it brings nothing more than what a .reduce(...) could bring already.

is what the intention was — nothing earth-shattering haha. A number of static utility methods exist just to make common patterns more expressive and direct. For example, Object.getOwnPropertyDescriptors could be implemented by combining other methods and a reducer, too. (Or for that matter, Array.prototype.map and Array.prototype.filter can also both be implemented as reducers.)

I think the ideas you’ve described are very interesting, especially with those last examples. I’m super intrigued but my reservation is that it seems quite removed from what this proposal was intended to cover.

@WebReflection
Copy link

@bathos maybe we agree that having now Object.fromEntries and tomorrow Object.from that also covers Object.fromEntries would make the former redundant/superfluous in the long term?

Again, I'm OKihs with fromEntries, I just hope we don't seal the opportunity with Object.from because of that.

@bathos
Copy link
Collaborator

bathos commented Mar 13, 2018

Yes, I’d agree (and perhaps the separation would also permit Array’s @@entries or similar from having to special case anything). I mentioned in the 'name' section that one reason fromEntries could be favorable over from is that the latter "could be inconsistent if future methods allow building objects 'from' other sources" — this sort of thing was what I had in mind when noting that.

Please feel free to keep exploring these ideas in the context of this proposal in any case — I had one thing in mind so I wanted to say so, but really it’s out of my hands now what direction it takes. I’m particularly curious to hear what @bakkot has to say.

@Loirooriol
Copy link
Contributor

imagine dataset can implement a Symbol.entries so that Object.from(node.dataset) can snapshot that state instead of holding a live proxy object.

@WebReflection I don't really see the advantage of Symbol.entries. Wouldn't it be the same as if dataset was iterable and you used Object.fromEntries?

@ljharb
Copy link
Member

ljharb commented Mar 13, 2018

I don’t think an Object.from would ever make Object.fromEntries obsolete; creating a snapshot is typically done with toJSON, since snapshots typically need to be serializable.

A generic way to provide entries might indeed make sense, but that current generic method is “an entries method that returns an iterator”.

@bakkot
Copy link
Collaborator

bakkot commented Mar 13, 2018

@WebReflection I find the idea of an entries protocol really interesting! I think if we get first class protocols (fingers crossed), then a builtin "entries" protocol which user code could implement and which would be consumed by Object.from (and new Map?) and/or some other methods would be a great idea.

I do think that it's probably out of scope for this proposal, mind; this proposal seems useful either way. An "entries" protocol would just make it easier for user code to play along.

@WebReflection
Copy link

WebReflection commented Mar 14, 2018

@Loirooriol what you say is basically: "wouldn't be the same if every object was iterable and returned entries?" to which I answer no.

It's counter-semantic to write Object.fromEntries(anotherObject / node.dataset) when these are representable as objects, not as entries.

Let's keep the semantic Object.fromEntries(Object.entries(node.dataset)) where it belongs and keep doors open for Object.from(generic)

@SeregPie
Copy link

SeregPie commented Mar 31, 2018

Array.from has a second argument, where you can provide a map function. Would be nice to have the same in Object.fromEntries.

@ljharb
Copy link
Member

ljharb commented Mar 31, 2018

@SeregPie true - #13 also wants a use for the second argument, though

@bakkot
Copy link
Collaborator

bakkot commented Mar 31, 2018

@SeregPie:

Since this accepts arbitrary iterables and produces something which is not an array, there's no obvious place for a mapping function to go, which suggests the mapping function argument could be useful.

On the other hand, I would strongly prefer standardizing some iterator adapters (in another proposal) and then expecting those to be used instead. The mapper argument is kinda dumb - wouldn't it be much clearer if we could write

Object.fromEntries(map(fn, x))

rather than

Object.fromEntries(x, fn)

?

Then no one has to learn about this non-obvious second argument, and you could do more powerful things (like filter) to boot.

(Where map is something like function* map(fn, iter) { for (const x of iter) yield fn(x); }.)

@ljharb
Copy link
Member

ljharb commented Mar 31, 2018

fwiw the Array.from mapper has turned out to be very useful, non-obvious or not.

@bathos
Copy link
Collaborator

bathos commented Apr 1, 2018

I’m also in favor of a distinct iterator utilities module for facilitating this (and more), but I don’t believe that having that plan really precludes the possibility of introducing a mapping arg here anyway.

I lean slightly towards avoiding it for principle of least surprise reasons. It doesn’t seem super obvious that there would be a second arg or what it would do. I mentioned in #13 that "single arg functions play nicely with Array.prototype.map out of the box"; @ljharb pointed out that supporting that shouldn’t be a specific design motivation. That may be true; my angle was footgun avoidance, since I’ve encountered bugs caused by using >1 arity functions as iterator function callbacks enough times to want to avoid creating new opportunities for them to occur. However I agree this is a pretty weak argument for avoiding the mapping argument.

@ljharb
Copy link
Member

ljharb commented Apr 4, 2018

#13 has been repurposed to cover a second argument as either a proto - like Object.create, or a mapper - like Array.from.

It seems like Object.fromEntries is still needed, with that name, and with the current semantics. Object.from seems like something a future proposal could add with or without fromEntries.

Is it alright to close this, and leave Object.from to a separate (parallel, or follow-on) proposal?

@ljharb ljharb closed this as completed Apr 14, 2018
@ljharb ljharb mentioned this issue Aug 21, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants