tc39 / proposal-explicit-resource-management

ECMAScript Explicit Resource Management
https://arai-a.github.io/ecma262-compare/?pr=3000
BSD 3-Clause "New" or "Revised" License
759 stars 30 forks source link

`using` as expression syntax, not binding statement #237

Closed pauldraper closed 2 months ago

pauldraper commented 2 months ago

For some reason, this proposal conflates variable binding with resource management.

Anonymous invocation

https://github.com/tc39/proposal-explicit-resource-management/issues/214, https://github.com/tc39/proposal-explicit-resource-management/issues/228, and others. Plus birthing a whole separate proposal (https://github.com/tc39/proposal-discard-binding) to address this issue.

Destructuring

https://github.com/tc39/proposal-explicit-resource-management/issues/77, https://github.com/tc39/proposal-explicit-resource-management/issues/78, and others

Verbose

An entire statement must be devoted to resource acquisition, always.

General complexity

Users must now know a new kind of binding. Is it mutable? What is the scope?

Can I have multiple assignments, like the other bindings?


This all can be prevented by having using as expression operation, like await.

using myResource(); // no binding
const { a, b } = using myResource(); // destructured biding
let { a, b } = using myResource(); // mutable destructured binding
{
  doThing(using resource1(), using resource2()); // subexpression
}

Objectively, the current proposal obsession with binding unnecessarily coupled and interacts poorly with existing syntax.

pauldraper commented 2 months ago

Note: It would make sense to include binding if using were a control structure like for of.

using (const value from myResource()) {
}

Which honestly, I prefer even more as it encourages scopes to be as small as possible.

But again, this is natural and consistent with existing ECMAScript syntax.

using (const { a, b } from myResource()) {
}

It's not too late to save this idea! We haven't gotten to stage 4 yet.

rbuckton commented 2 months ago

For some reason, this proposal conflates variable binding with resource management.

That is how this is managed across numerous languages, including C++, C#, Python, and Java. Tying resource lifetime to a block scoped variable binding is extremely convenient as it very clearly defines resource lifetime as being bound to the enclosing {}.

214, #228, and others. Plus birthing a whole separate proposal (https://github.com/tc39/proposal-discard-binding) to address this issue.

Discards were made into a separate proposal because there are more use cases for discards than just using.

An entire statement must be devoted to resource acquisition, always.

Due to the action-at-a-distance nature of resource cleanup, it was a requirement that we have a clearly labeled statement that is easy to recognize when scanning code.

Users must now know a new kind of binding. Is it mutable? [...]

Can I have multiple assignments, like the other bindings?

using declarations are like const. They cannot be reassigned as that would make it very confusing as to what actually gets disposed.

What is the scope?

Just as with const, using declarations are block scoped. As I mentioned earlier, block scoping is an intrinsic part of managing resource lifetime.

This all can be prevented by having using as expression operation, like await.

using cannot be an expression, that is something we've already discussed in several other issues and has already been discussed in committee. Due to the action-at-a-distance nature of cleanup, it was important to several TC39 delegates that it be very clear when code could introduce cleanup at the end of a block.

using myResource(); // no binding

This is not feasible as Expression could be a parenthesized expression, and using (expr) is already a legal function call.

const { a, b } = using myResource(); // destructured biding
let { a, b } = using myResource(); // mutable destructured binding
{
  doThing(using resource1(), using resource2()); // subexpression
}

You can use a DisposableStack for these cases:

using stack = new DisposableStack();
const { a, b } = stack.use(myResource());
doThing(stack.use(resource1()), stack.use(resource2()));

This maintains the requirement that the cleanup effects that occur on scope exit are clearly labeled as a statement, but still allows you to track resources within subexpressions.

Note: It would make sense to include binding if using were a control structure like for of.

using (const value from myResource()) {
}

This is very similar to the syntax we proposed for this in Stage 1, and the syntax has undergone numerous changes based on feedback over the years. The current syntax is the form we've chosen after years of discussion, debate, and experimentation. We chose to switch to an RAII-style syntax as it was strongly favored by many on the committee.

But again, this is natural and consistent with existing ECMAScript syntax.

The syntax as proposed is consistent with const, and naturally reads as a variable declaration (which it is). using (const x of y) has more in common with a for..of statement, which is intended to express iteration, not declaration.

using (const { a, b } from myResource()) {
}

We expressly forbid destructuring because users would be very confused as to what is actually being disposed. One user might expect it is the result of myResource() that is disposed, while another might expect it is a and b that are disposed, and delegates argued in both directions.

We've had many discussions about resource tracking and reliability over the past few years. If you want reliable resource tracking, you must ensure allocation occurs in a known order, and that you track the resource as close to its allocation as possible. The further away from allocation you are when you track the resource, the greater the likelihood that an exception could occur before the resource can be tracked. This is why tracking a and b above would be bad, as its completely possible for both resources to be allocated, but for an exception to occur in between tracking each binding, for example: using (const { a = foo(), b } from myResource()) .... Here, b could be allocated, but foo() could throw before b is tracked.

While this should clearly illustrate why we must track the value of myResource() and not the bindings, we opted to ban destructuring entirely to avoid user confusion.

It's not too late to save this idea! We haven't gotten to stage 4 yet.

This proposal has gone through many rounds of revisions and discussions in plenary over a number of years. In that time, we've already investigated the suggestions provided here and decided against them. Now that we are at Stage 3, unless there is a major concern we are unlikely to make further changes to the syntax.

Per the process document, at Stage 3:

The proposal has been recommended for implementation. No changes to the proposal are expected, but some necessary changes may still occur due to web incompatibilities or feedback from production-grade implementations.

That is not saying there cannot be changes if relevant concerns are raised, but there is usually a very high bar for any major change to a proposal this late in the process.

Since all of these suggestions have already been discussed previously, I am closing this issue.