-
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
Allow destructured LHS #78
Comments
In that case tho, is the disposal called on the object being destructured, or on each destructured item? |
It would be called on the object being destructured. |
I believe this proposal currently allows null-ish. And I also agree that since the |
You could possibly allow If you look at the resource as being attached to the variable itself then I agree it could be confusing, but given that the proposal already explicitly calls out unbound resources, I'd argue that it actually behaves more like a deferred operation on the scope (so I'd also suggest the terminology could be improved by actually writing it as "defer", but that's a separate issue). Under that lens, it makes pretty reasonable sense that the whole destructured object provides the deferral. |
Destructuring is explicitly ignoring the object. It would be very strange to do anything with it in this scenario, including disposal. |
I think any attempt to reconcile disposal and destructuring will inevitably end up reinventing #77 (comment) in some form (maybe with a different keyword/marker denoting disposal, and maybe with |
Well to be fair my suggestion to do this entirely in user-land or as a built-in API instead of a syntax addition doesn't have this limitation. With such an approach, the disposal tracking is done as a passthrough function, so you're entirely free to destructure the passed-through value, or ignore it altogether if you just need disposal. It does that without sacrificing readability since it's clear that the resource tracked is the one on which the function is called. |
I'll weigh in on this and say that having different allowed syntax in Similarly, as a developer I would expect that using const a = getA(), b = getB(); is equivalent to using const a = getA();
using const b = getB(); (I don't think I'm missing anything there?) In my mind, const $temp = initialiser;
defer $temp[Symbol.dispose]; // borrowing from Go
const target = $temp; and it would be entirely reasonable to use allow destructuring patterns as |
Maybe we could consider destructuring at a later point, but I agree with the consensus that destructuring should be forbidden for now. One developer's intuition on what should be disposed when destructuring may not match another developer's intuition. For now, there are three options you can employ: // (a) `expr` is resource:
using const temp = expr;
const { x, y } = temp;
// (b) `x` and `y` are resources (safe, if getter for `y` throws, `x` is still disposed):
const temp = expr;
using const x = temp.x, y = temp.y;
// (c) `x` and `y` are resource (unsafe, if getter for `y` throws, `x` is not disposed):
const { x, y } = expr;
using const void = x, void = y; Option (c) could be unsafe depending on the implementation of |
Adding |
I doubt the TC39 would consider such small addition to be worthwhile, if we don't do it right now the inconsistency will probably stay around.
Is it |
If not aiming for a grammar that is consistent with using x = getX();
using void getY(); This would make it seem more reasonable that destructuring is not available, and in addition nobody would ask questions about |
Unfortunately, this wouldn't work the way you describe. That said, we could conceivably drop I would, however, correct your example to the following: using x = getX();
using void = getY(); I'd prefer leaving the using x = getX(),
void = getY(),
z = getZ(); Dropping the using Type name = initializer; It would also reduce the number of characters needed for using await x = getX(); |
While not the common use case, I would argue that a getter that lazily initializes a resource isn't an anti-pattern. The purpose of this proposal is to give a user more control over resource lifetime. Letting a user avoid allocating a resource that may not be used is in keeping with that principle, so I wouldn't want to ignore that use case for the sake of convenience. |
Ah, good point, but as you say one could make this work as long as we only allow identifiers following
I think it sounds/looks more natural without the using x = getX();
using void getY();
using z = getZ(); if you want to mix them. Unless we do #77 for consistent introduction of |
Sorry for my confusing phrasing, I agree with you there. What I considered to be an antipattern was function getResources() {
const x = openX();
x[Symbol.dispose] = closeX;
const y = openY();
y[Symbol.dispose] = closeY;
return {x, y};
}
…
const res = getResources();
using x = res.x, oops = res.will.throw, y = res.y; // y is not disposed It should be function getResources() {
using stack = new ExitStack();
const x = stack.push(openX());
const y = stack.push(openY());
return {x, y, [Symbol.dispose]: stack.deferDisposer()};
}
…
using res = getResources();
const x = res.x, oops = res.will.throw, y = res.y; // y is disposed fine Getters (or get methods) are fine if used like function getResources() {
return {
get x() {
const x = openX();
x[Symbol.dispose] = closeX;
return x;
},
get y() {
const y = openY();
y[Symbol.dispose] = closeY;
return y;
},
};
}
…
const res = getResources();
using x = res.x, oops = res.will.throw, y = res.y; // y was never opened or function getResources() {
const stack = new ExitStack();
return {
get x() {
return stack.push(openX());
},
get y() {
return stack.push(openY());
},
[Symbol.dispose]() {
stack[Symbol.dispose]();
},
};
}
…
using res = getResources();
const x = res.x, oops = res.will.throw, y = res.y; // y was never opened, x is disposed fine |
This proposal previously used I feel that |
|
Given the various discussions in plenary and the possible confusion between what would be disposed (e.g., the Initializer or the BindingPattern), I'm not inclined to support destructuring in the LHS. It is much clearer to be explicit about what is to be disposed, so it would be better to have a secondary step: {
using const resource = ...; // `resource` is what will get disposed
const { x, y } = resource;
} // `resource` is disposed Alternatively, you can leverage a {
using const stack = new DisposableStack();
const { x, y } = stack.use(resource); // `resource` is what will get disposed
} // `stack` (and therefore `resource`) are disposed |
Thinking about the
using const void
case made me realize we already have a syntax for assigning the result of something to nothing (provided it's not nullish,which would already be an error here):const {} = ...;
.Given that, the thrown-away value case falls right out if we simply allow destructuring in the LHS of the
using const
statement:This would increase syntactic consistency in the language (destructuring is allowed after any
const
) rather than decreasing it (by suddenly allowingvoid
in a weird new place), and adds convenience and potentially useful new patterns if only one or a few properties/elements are needed from the disposable object:(Note: it's also interesting consider allowingfor (using const elem of ...)
but I worry about potential surprises about whether the iterable itself or the individual elements are disposable - syntax consistency would suggest the latter, while it would be nice to have the former.)EDIT: added some strikethrough
The text was updated successfully, but these errors were encountered: