-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stage 3 Specification Review: Shu-yu Guo #93
Comments
@syg, please review the current specification text prior to the September meeting, if possible. |
I've had a look at the new spec text. I have some questions, mostly about the new built-in constructors.
|
Thanks for the pointer @zloirock. For the first bullet point, the motivations I see are "it exists in Python" and "it's useful". I'd like @rbuckton to expand on that a bit. For the second bullet point, I'm not sure I see a reason for why it's auto-bound, just that it is auto-bound. Perhaps it's implicit in that explanation about NumberFormat. I don't recall why NumberFormat's |
https://github.com/tc39/proposal-explicit-resource-management#assist-in-complex-construction. Large-scale projects will often create components that consume multiple resources, the lifetimes of which are bound to the lifetime of the component. In the simplest case,
Consider the class PluginHost {
#channel;
#socket;
constructor() {
using channel = new NodeProcessIpcChannelAdapter(process);
using socket = new NodePluginHostIpcSocket(channel);
// these get disposed as soon as the constructor exits, so assigning them here isn't very useful
this.#channel = channel;
this.#socket = socket;
}
} If class PluginHost {
#channel;
#socket;
constructor() {
this.#channel = new NodeProcessIpcChannelAdapter(process);
this.#socket = new NodePluginHostIpcSocket(this.#channel);
}
[Symbol.dispose]() {
this.#socket[Symbol.dispose]();
this.#channel[Symbol.dispose]();
}
} But there are multiple issues here. If an exception occurs when creating class PluginHost {
#channel;
#socket;
constructor() {
this.#channel = new NodeProcessIpcChannelAdapter(process);
try {
this.#socket = new NodePluginHostIpcSocket(this.#channel);
} catch (e) {
this.#channel[Symbol.dispose]();
throw e;
}
}
[Symbol.dispose]() {
try {
this.#socket[Symbol.dispose]();
} finally {
this.#channel[Symbol.dispose]();
}
}
} This kind of boilerplate class PluginHost {
#disposables;
#channel;
#socket;
constructor() {
using stack = new DisposableStack(); // this is guaranteed to dispose at end of block
this.#channel = stack.use(new NodeProcessIpcChannelAdapter(process));
this.#socket = stack.use(new NodePluginHostIpcSocket(this.#channel));
this.#disposables = stack.move(); // move out of stack that will dispose now and into a stack we can dispose later
}
[Symbol.dispose]() {
this.#disposables[Symbol.dispose]();
}
} In this example, we create a
This was requested feature to better work with factory functions.
No. It's my understanding that many on the committee hope that species can eventually be removed, but until that occurs I felt it was best to specify it and possibly just remove it later if necessary.
An alternative to try {
using r1 = { [Symbol.dispose]() { throw new Error("c"); };
using r2 = { [Symbol.dispose]() { throw new Error("b"); };
throw new Error("a");
} catch (e) {
e // AggregateError {
// cause: AggregateError {
// cause: Error { message: "a" },
// errors: [Error { message: "b" }]
// },
// errors: [Error { message: "c" }]
// }
} Or a new Error subclass (e.g., |
VS Code has hundreds of examples of this, not to mention hundreds of extensions that use VS Code's |
That's something I intended to talk more about when presenting, but I do show an example of the "factory function" use case for a bound |
Just to add a big +1 from me on having |
@syg, is it your recommendation that I remove the |
That's my preference, yes. Are there compelling use cases for that subclassing? I like the |
Why would removing them prevent subclassing? It might change which methods need to be implemented on a subclass, sure, but that's not the same as preventing it. |
If we go with (2), since To be fair, I'm not sure how often subclassing would make sense. |
Option 1 is how a lot of things already work, and that seems perfectly reasonable to me (especially if there's a fallback in the event of a non-callable constructor property to But yes, ime subclassing rarely makes sense anyways, especially on builtins. |
Or it could construct a new subclass instance and store the base DisposableStack in the new subclass instance. i.e. class DisposableStackSubclass extends DisposableStack {
move() {
let ret = new DisposableStackSubclass();
ret.use(super.move());
return ret;
}
} No prototype patching required, works even if the subclass has private fields, and no difference from a consumer's point of view. Plus it has the advantage that the overridden |
What means "a lot"? How many instance methods, at least some examples? |
@bakkot why enforce users to wrap all methods of prototypes if they need subclasses? In some cases, like arrays, it can be dozens of methods. Why create extra instances and do extra work just for that? In case if prototypes will be extended by some methods in the future - those methods can be not wrapped by library authors that create those subclasses - but final users who think that it's a proper subclass will await subclass instances as results of those methods. Etc. Too many problems and horrible user experiences for built-ins subclassing. |
The proper subclassing (including built-ins) with instance methods was one of the greatest attainment of ES6. |
The latest example - a couple of weeks ago, in the latest Babel release, was added "RegExp duplicate named capturing groups" transform - and it properly works only because of those patterns. I don't see how such features could be properly (with all string, regexp methods, etc.) implemented without these patterns. Maybe such flexibility is a controversial moment for |
@zloirock I would ask the question in the other direction: why should we add overhead to the language and implementations just to make it simpler to subclass DisposableStack? It's not hard to subclass without any extra help from the language. While I used to agree with you about the argument that new methods might break the subclass, I no longer find it particularly compelling, because new methods can break the subclass anyway by failing to uphold its invariants. It's just not practical to guarantee that new methods added to the base class will be coherent on existing subclasses before those subclasses are updated to take into account the new methods. Plus I don't expect subclassing DisposableStack to be that common. Even Map and Set, which are the classes most suited for subclassing currently, are rarely subclassed, and frequently those subclasses are straight-up broken already by the one existing feature intended to simplify subclassing Map (i.e. the fact that the Map constructor calls |
@bakkot why do you think that it will cause any significant overhead? A simple check that the
I think that it's a strange understanding of subclassing. I (and I think many usual users too) expect that subclassing will work right out of the box everywhere - on instance methods too.
I didn't expect that |
I didn't say the overhead would be large. But it is overhead, and I don't think the overhead is justified. The benefits are very small.
It "works" in the sense of "you can make a subclass". But if the base class adds a new method, the subclass's invariants might be broken if users start invoking the new methods on the subclass. That's unavoidable. |
Everything causes overhead - why do you think that exactly the proper subclassing is a feature worth sacrificing for a tiny performance improvement?
If instance methods of this subclass will return the base class instances - that will mean that it "partially works".
I don't think that such collisions are related to this topic. Static methods, new constructor arguments, and many other things could cause such collisions. |
Again, I would turn it around. Why do you think that simplifying the process of subclassing Also, the concern is about complexity, in addition to performance performance. We'd be making the implementation of this method more complicated. Even if it were implemented with no performance overhead at all, complexity is a cost in itself.
Is your position that subclassing does not fully work in any language which doesn't have |
Just for ensuring the proper subclassing everywhere where it's possible and does not cause any significant problems like significant overhead. Just for writing
My position is that JS by ES6 has a good subclassing system - definitely better than in many other languages. But now I see regular attempts to kill it or at least make it worse. |
Subclassing a Promise doesn't make any sense because every |
|
@ljharb so, maybe you will answer? |
@zloirock You've already listed them. The reality here is that this debate - the merits of subclassing, what constitutes "proper" subclassing, and whether Symbol.species is actually a good mechanism to achieve subclassing - is one that hasn't been resolved even in committee. However, the majority of the committee seems to consider RegExp subclassing to be a mistake (but the majority doesn't think it's worth trying to remove it, unfortunately), and the majority of the committee seems to prefer to see Symbol.species removed if it is web-compatible to do so. New APIs shouldn't add something we're hoping to remove, even for consistency, and that includes Symbol.species. |
So, by "a lot" you meant "no one"? I agree that for
So, the committee wanna (?) degrade the language that they should improve. Clear.
Is this a call for me to make it completely incompatible with the web? It looks like a provocation. |
In general the concern is like 90% complexity that has had, historically, caused security issues. |
I put together #114 to address removing Symbol.species support. |
Also trying to close the loop on the I see the justification linked above is ease of use in factories, with this example given: function makePluginHost() {
using const stack = new DisposableStack();
return {
channel: stack.use(new NodeProcessIpcChannelAdapter(process)),
socket: stack.use(new NodePluginHostIpcSocket(this.#channel)),
[Symbol.dispose]: stack.move().dispose,
};
} I am not really understanding that example. Why is the function makePluginHost() {
const stack = new DisposableStack();
const channel = stack.use(new NodeProcessIpcChannelAdapter(process));
const socket = stack.use(new NodePluginHostIpcSocket(this.#channel));
return {
channel,
socket,
[Symbol.dispose]: stack.dispose.bind(stack),
};
} Is that really that much less ergonomic as to harm adoption? |
Your version is an example of faulty construction and a potential resource leak: If an exception occurs when constructing In my version, if construction of |
Ah, exception safety! Thanks for the correction. It'd be something like the following then? function makePluginHost() {
using const stack = new DisposableStack();
const channel = stack.use(new NodeProcessIpcChannelAdapter(process));
const socket = stack.use(new NodePluginHostIpcSocket(this.#channel));
// Should not throw after this point.
const movedStack = stack.move();
return {
channel,
socket,
[Symbol.dispose]: () => { movedStack.dispose(); },
};
} |
Essentially, yes. |
With the auto-binding method removed and the jagged errors behavior merged with I have editorial comments about the spec draft but that can wait for later. The major editorial comment is that you define "interface" in this section and then list only the disposables as interfaces, while there are other interfaces already. If you want the spec to formally define interface, it might be better done as a pre-req step. |
I've removed the text from this proposal. If we need to pull that text out of the Iterator section into somewhere else, I can do that when I put together the PR for ECMA262. |
This is a placeholder task for the Stage 3 Specification Review feedback from Shu-yu Guo (@syg), per the October 2021 Meeting
The text was updated successfully, but these errors were encountered: