tc39 / proposal-call-this

A proposal for a simple call-this operator in JavaScript.
https://tc39.es/proposal-call-this/
MIT License
121 stars 5 forks source link

Interoperability with symbol-based protocols #4

Closed js-choi closed 3 years ago

js-choi commented 3 years ago

In tc39/proposal-extensions#11, @bmeck wrote the following:

i think the bind operator changes the general flow and are bigger refactors from normal JS workflows and don't necessarily have the path towards dealing with protocols that [the Extensions] proposal does.

other bind proposals take an expression and bind the values within that expression. [The Extensions] proposal allows dispatch to match the normal receiver of an expression during invocation using a value not on the receiver. Other bind proposals do not allow lexical receivers in normal JS position and instead use currying.

Because this repository is meant as a simplified successor to the old bind-operator proposal and as an alternative to tc39/proposal-extensions, I opened an issue here.

Having said that, I am not quite sure I understand the quoted concerns. This proposal works with symbol-based protocols. And, in this proposal, no arguments (other than the this value) are bound.

Either of these would simply work:

y->(Array.prototype[Symbol.iterator])();
const $arrayIteratorFn = Array.prototype[Symbol.iterator];
y->$arrayIteratorFn();

CC: @ljharb

bmeck commented 3 years ago

It would be good to understand how this proposal works on the following example:

for (let key of map.keys()) {
}

Right now it looks like it would be rewritten using this proposals as

for (let key of {[Symbol.iterator]: map->(Map.prototype.keys)}) {

}

This in turn exposes the mutable Map Iterator instance that doesn't really have a way to protect against .next mutation. E.G.

// stops the loops
Object.getPrototypeOf(new Map().entries()).next = function () {return {done: true, value: 'hi'};};
const myMap = new Map([[1,1],[2,2]]);
for (const x of myMap) {
  console.log({x})
}
for (const x of myMap) {
  console.log({x})
}
for (const x of myMap) {
  console.log({x})
}

I do agree for functional approaches that this proposal does enhance the ability to write robust code but it seems difficult / unlikely to actually protect against OO robustness and could lead to confusion.

ljharb commented 3 years ago

Yes, due to the design of MapIterator this proposal doesn't address it directly.

However, you could do const mapKeys = new Map().keys()[Symbol.iterator]; map→keys()→mapKeys() just fine, so i'm not sure what the concern is.

bmeck commented 3 years ago

That would still fail to execute the loops. I'm stating that this proposal should be wary of OO patterns which are often off of Symbol protocols since it does cause problems. These OO problems are much harder to mitigate than the existing bind pattern. I'm unclear a bit on the claims of robustness holding up as a goal if it leads to pitfalls like this. The issue was about problems, which still exists. Wether it is in scope or not is not really easy to discern without going to plenary, but I would state at least from the readme this is a concern given that it gives a false sense of robustness.

ljharb commented 3 years ago

Aside from Symbol.iterator, Symbol protocols are hardly ever used - but I’m still very confused here. Symbols and strings are identical.

I see how it gets tricky if the code you’re writing uses one method access (string or symbol) to produce a new object, on which you then do another method access (string or symbol) - but that’s the same problem no matter what kind of property key is used.

bmeck commented 3 years ago

Maybe we can just rename this to interoperability with OO based protocols?

ljharb commented 3 years ago

It’s not just OO-based tho - that’s what Array slice is. It’s the specific pattern of one method in an X returning a new kind of object Y, which then needs its own methods saved with this operator.

This is a rare pattern other than the builtin iterator types, but again, for those, i think you could still do the same things - it’s just trickier to get at them since they’re not globals in the first place. Luckily there’s another proposal for getting intrinsics :-p

bmeck commented 3 years ago

Perhaps interoperability with things that return Objects?

ljharb commented 3 years ago

Much better, but it’s even a tiny subset of that - things that return objects that aren’t directly accessible (globally, or importable/requireable).

bmeck commented 3 years ago

@ljharb I don't think indirect access is necessary since Map.prototype for example is directly accessible and since this proposal predicates early access anyway you can just call it eagerly to get the stuff.

ljharb commented 3 years ago

Right but there’s no ergonomics issue with Map.prototype - only with, eg, MapIterator.prototype, unless I’m misunderstanding something. (this all ofc assumes first-run code)

js-choi commented 3 years ago

I’m going to close this problem as out of scope. Although improving the Node primordials situation is one of the goals of this proposal, I’ve refocused its greater thrust onto “bind/call/apply are very common but they are clunky” (#8). This problem seems to be pretty rare even within the primordials use case, anyway. Let me know if there’s anything else actionable that we can do here.