tc39 / proposal-extensions

Extensions proposal for ECMAScript
MIT License
150 stars 6 forks source link

Useful to Node.js Core #11

Open bmeck opened 3 years ago

bmeck commented 3 years ago

Node contains a lot of methods that are uncurry the value of this in order to safely obtain functionality like [0, 1].slice() that is not subject to prototype pollution. This proposal would effectively have a lexical way to perform the uncurryThis usage of those primordials.

// instead of
const slice = Function.prototype.call.bind(Array.prototype.slice);

function copyArray(arr) {
  return slice(arr); // changes ordering of syntax and diverges from common coding appearance of JS
}

// you could do
function copyArray(arr) {
  return arr::slice();
}

CC: @ljharb

This wouldn't cover all the use cases but is potentially a solution to an ongoing problem Node is facing regarding maintainability of parts written in JS needing to be robust. I wonder if we could expand this proposal just a little to intercept protocols like Symbol.iterator as well as that would greatly simplify parts of Node's internals and migrate towards something that looks like more idiomatic JS.

// currently
for (let x of y) { // unsafe due to prototype mutation or shadowing potentially in `y`
}

// something (bikeshed)
// this would basically need to shadow the properties used by protocol hooks to be the robust/safe/well known forms
for (let x of y::{Symbol.iterator: Array.prototype[Symbol.iterator]}) {
}
for (let x of y::{...Array.prototype}) {
}
ljharb commented 3 years ago

Completely agree with the first para; the original bind operator as well as the proposal @js-choi plans to present at the next agenda also help address node’s primordials use case in the same way as this proposal, which is also my use case, and the reason i wanted to ensure bind wasn’t dead before pipeline advanced.

bmeck commented 3 years ago

@ljharb 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 this proposal does.

ljharb commented 3 years ago

As for the symbol, you could do:

const iter = Function.call.bind(Array.prototype[Symbol.iterator]);
for (const x of o::iter()) { }

without any scope expansion.

ljharb commented 3 years ago

I’m not clear on how all three of the bind operator proposals are any different wrt protocols, which are methods that need to be syntactically called with a temporary receiver the same way as string methods.

bmeck commented 3 years ago

@ljharb other bind proposals take an expression and bind the values within that expression. This 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.

ljharb commented 3 years ago

I think we have very different understandings then. At the least, the original one and the one intended to be on the next agenda allows the equivalent of a::b(c) to call b with a as the receiver and c as an argument.

js-choi commented 3 years ago

For reference: The alternative proposal to which @ljharb refers to is js-choi/proposal-bind-operator. I plan to propose it at the next plenary. I have been engaging with @hax to see how much we can reconcile our proposals (although we haven’t been able to talk about it recently).

I’m not sure what the concern with regards to protocols is, but talking about other proposals might be off-topic to this repository, so I opened js-choi/proposal-bind-operator#4.

hax commented 2 years ago

@bmeck I think the use case could be easily solved by:

const ::{slice} from Array

function copyArray(arr) {
  return arr::slice();
}

or

// extract all methods from Array and convert to a frozen extension, 
// so could use A as namespace for all array methods
const A = Extension.from(Array) 

function copyArray(arr) {
  return arr::A:slice();
}

About symbol, because Array.protoype[Symbol.iterator] is same as Array.prototype.values, so u could just use

for (let x of y::A:values()) {
}

or

const ::{slice, values} from Array

for (let x of y::values()) {
}

Maybe we need general syntax for symbols if we have some use cases which must rely on the original symbol keyed methods.

For example:

for (let x of y::A:[Symbol.iterator]()) {
}

But I doubt symbol based behaviors are hard to be robust, for example str.startsWith(obj) will check obj[Symbol.match], not clear how we can avoid be affected by modified [Symbol.match] values.

ljharb commented 2 years ago

startsWith does not call into Symbol.match.

hax commented 2 years ago

@ljharb It does.

str.startsWith(obj) will check obj[Symbol.match]

If there is truthy value, startsWith will throw. Having Symbol.match means it's an regexp-like object, and startsWith/endsWith/includes do not accept regexp-like objects as the argument.

ljharb commented 2 years ago

That’s only if Type is Object, which it’s not for a string primitive. I do see that you named the arg “obj” tho, so maybe you were only talking about when passing an object.

In node core, startsWith would only be used for a string argument, for this reason.

hax commented 2 years ago

@ljharb But the defensive code do not know whether it's object or primitive ? Or at least need to check type before using.

Anyway, even in this case it's ok, but essentially symbol means some protocol, and one method may rely on that symbol and call the symbol properties/methods on the argument, so it seems the symbol properties/methods are meant to be virtual/dynamic and hard to be ensured in some static way --- this is what I ask.

ljharb commented 2 years ago

Defensive code does extensive type-checking for the reasons mentioned :-)

hax commented 2 years ago

Oh, that's another reason we need strong brand check :-)

hax commented 2 years ago

After rethink of the symbol, I realize that the problem is not whether it's symbol or string, but whether the keyed methods are tend to be static or virtual (aka. leave for subclass to override). So either we need syntax for extract and invoke the symbol keyed methods/accessors, or we need convenient way to extract the special protocol implementation (see first-class protocol proposal) of a class into a extension, for example: const ArrIter = IteratorProtocol.extractImplementationFrom(Array); for (let x of array::ArrIter:iterator()) { ... }. Or we could have both.

But the static/dynamic invoking problem also lead us to another important use case of :: notation --- write static call easily for defensive usage, not only outside but also internal. Currently only private methods/accessors are static, so if you want you classes as robust as platform, and could be used defensively outside like node.js do, you have to write all code in private methods, then wrap them as public methods. This is cumbersome. Decorators may help (hope so), but if I understand correctly, decorating methods not enough, also need decorate the whole class. And such pattern (decorate every member and the class) seems still cumbersome. A possible solution is extending this proposal to allow this::method() in the class mean statically call the original public method defined in the class. So it's easy for people write a robust class without too much effort.

ljharb commented 2 years ago

I don’t think we need any special-casing for the weird regex symbol behavior, or that Set/Map calls add/set; that’s just something people need to know.

hax commented 2 years ago

or that Set/Map calls add/set

Could u say more about this? I don't get it.

ljharb commented 2 years ago

Constructing a Map or a subclass calls this.set, and constructing a Set or subclass calls this.add, both observably.

hax commented 2 years ago

Constructing a Map or a subclass calls this.set, and constructing a Set or subclass calls this.add, both observably.

Oh NO.... Isn't it a spec bug? I can't understand why the constructor call this.set/add instead of private methods...

hax commented 2 years ago

that’s just something people need to know.

Obviously I don't know it... and I can say at least 95%+ developers don't know it. 😢

hax commented 2 years ago

Oh , I guess it's for subclasses...

bmeck commented 2 years ago

I'd be wary of reliance upon it, it has been brought up a couple times as something potentially needing to break to fix various other subclassing issues.

ljharb commented 2 years ago

Both true - it's for subclasses, but since "subclassing builtins in JS" has a wildly inconsistent pattern/model, and it'd be nice to clean it up, it's best not to rely on it - meaning, a Map/Set subclass should always define its own set/add, respectively.