-
Notifications
You must be signed in to change notification settings - Fork 30
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
Using binding #15
Comments
Filling in some more from backchannel IRC during the presentation:
{
using x = something();
x = somethingElse();
}
// is the result of `something()` disposed? what about the result of `somethingElse()`? both?
|
or |
If we want to allow mutability, we could instead associate const x = using something();
let y = using something();
y = somethingElse(); |
@ljharb I'm not 100% opposed to that, but I do like the natural readability of |
So, the issue here is that for the using (acquireResource()) {
doStuff();
} vs. {
using const justWaitingForThisToGoOutOfScope = acquireResource();
doStuff();
} This is a pattern you see a lot in C++ codebases (e.g. I realize this post would be more compelling with an actual example of something where you don't need to use it inside the block. I've seen many such cases, but can't recall one off the top of my head. @rbuckton may have some ready. |
A
|
@domenic A good example would be the classic lock one: using (await lock.acquire()) {
const state = await getState()
const newState = await changeState(state)
await writeState(newState)
} |
could just allow: {
using lock();
using const x = ...;
...
} but its a bit awkward since variable declarations don't return anything |
We would definitely need an NLT after the using. I'm not sure this issue is very bad. It seems similar to the one introduced by |
I'm also worried this introduces even more confusing semantics: {
using const a = …;
// some code
using const b = …;
} Both Conversely: using (const a = …) {
// some code
using (const b = …) {
}
} This syntax is much clearer and more explicit about scoping and lifetime, and aligns with how we initialize bindings in statements like |
Those are good points @rbuckton. I like the C#-style |
Please note that this proposal has moved to using the |
Would like to mention that C# 8 has introduced using declarations and Java has Project Lombok's @Cleanup annotation. |
If we could have used {
try const x = ...;
} As mentioned earlier, only having Does anyone think |
As an additional bit of information: C++'s P0709 R2 proposal would use the prefix |
Project Lombok's |
Another option would be to split the difference: // try-using statement for existing value
try using (expr) {
}
// try-using statement for new declarations
try using (const x = ...) {
}
// block-scoped using declaration
{
using const x = ...;
} |
if |
I disagree, Domenic's example in #15 (comment) illustrates this. We should have a form that does not require a binding, and |
@rbuckton makes sense 👍 |
what if “try using” didn’t create any bindings, but just took them? like: try using (x, y, z) {
const x = 1;
let y = 2;
var z = 3; // maybe?
const a = 4; // not disposed
} and it’d only allow identifiers that were declarations inside the try block? (or allow none, ofc) |
That has the same problem as |
What if, instead of a new It would require no grammar/syntax changes, avoids compatibility issues, avoids unnecessary bindings, and lets you avoid adding unnecessary additional scopes. Example: import {using} from 'std:using';
function foo() {
using(lock.acquire());
const file = using(openFile("foo.txt"));
doStuff(file);
} This approach also opens up another possibility: the concept of scope disposer functions that can be passed around. A downside of the What if, when calling a function like import {defer} from 'std:defer';
const at_exit = process.on.bind(process, 'exit');
const no_dispose = () => {};
function openFile(filename, disposer) {
const file = _privateOpenFile(filename);
disposer(() => closeFile(file));
return file;
}
function foo() {
const file1 = openFile("foo.txt", defer); // closes at the end of the current scope
const file2 = openFile("foo.txt", at_exit); // closes when the program exits
const file3 = openFile("foo.txt", no_dispose); // never auto closed
} And of course, import {defer} from 'std:defer';
function foo() {
console.log("Entering foo");
defer(() => console.log("Exiting foo"));
doStuffThatMightThrow(); This will look very familiar to folks who know Go. But it's way more powerful, since you can pass For any scope that includes Thoughts on this approach? |
@rbuckton It is tedious to create the new scope only for automatic disposal. function blah() {
use const foo = new FooHandle();
use const bar = new BarHandle();
use const baz = new BazHandle();
// ... Almost no changes to standard code layout. C++ RAII concept in its beauty!
// Automatic cleanup
} instead of function blah() {
try use (
const foo = new FooHandle(),
bar = new BarHandle(),
baz = new BazHandle()
) {
// ... Too much indentation, why do I need this new scope?
// Automatic cleanup
}
} And what are your thoughts on switching to |
“use” is a verb here; “try using” works together in English but “try use” doesn’t. |
@rbuckton @ljharb Ok, you may have a point. I created a related lightweight |
I've been considering this a bit more, including how to leverage this for values as well: {
using const x = expr; // dispose 'x' at end of block (EOB)
using const { a, b } = expr; // dispose 'a' & 'b' at EOB
using value expr; // dispose value of 'expr' at EOB
...
} At declaration:
At EOB/exception:
Pros:
Also, I've been considering introducing a {
const file = openFile();
using value new Disposable(() => file.close());
...
} More verbose, obviously, but fairly flexible. I'm not sure whether it would be necessary to introduce an |
If we like Also, I've gone back and forth about how I feel about how destructuring should be handled in these cases. Should we consider restricting CC: @leobalter, @dtribble (who have also expressed interest in the syntax). |
@littledan ambiguity with ParenthesizedExpression: |
At first, I like the idea of this: {
using const x = ...
} // dispose values at the end of block but this leads me to the question where disposing is also very useful on abrupts... so I wonder how we dispose resources. If that means: let called = 0;
{
using const x = { [Symbol.dispose]() { called += 1; };
}
assert.sameValue(called, 1); what goes in here? let called = 0;
try {
using const x = { [Symbol.dispose]() { called += 1; };
throw 'foo';
} catch {
assert.sameValue(called, 1); // Is this correct?
} The moment values are disposed are really the key for me to understand the whole thing here. I like the content from #15 (comment), btw. |
@leobalter I think that the disposal would happen after the |
I'd expect it to happen at the end of the try block, when it leaves scope. |
As @devsnek says, the disposal would happen at the end of the |
To clarify, @leobalter's example: let called = 0;
try {
using const x = { [Symbol.dispose]() { called += 1; };
throw 'foo';
} catch {
assert.sameValue(called, 1); // Is this correct?
} Would be essentially the same as this: let called = 0;
try {
const x = { [Symbol.dispose]() { called += 1; };
const $tmp_has_x = x !== null && x !== undefined;
const $tmp_x_dispose = $tmp_has_x ? x[Symbol.dispose] : undefined;
if ($tmp_has_x && typeof $tmp_x_dispose !== "function") throw new TypeError();
try {
throw 'foo';
}
finally {
if ($tmp_has_x) $tmp_x_dispose.call(x);
}
} catch {
assert.sameValue(called, 1); // Is this correct? yes, dispose has been called
} |
I stand corrected :) |
Thanks for the clarification, @rbuckton. I don't have any objections, it LGTM. I like it does offer consistency with any other usage within blocks. And a try catch should be manageable from within: {
using const x = { [Symbol.dispose]() { this.close(); }, ... };
try {
connect();
// For common usage, we are not trying to catch the parts from `using`,
// which seems like just a setup
} catch (e) {
// capture errors from connect() (or whatever code)
}
} // disposed here Although, I believe there might be concerns from other people where we need the dispose happening after a catch. I'm fine one way or another, we're in a good path syntax wise. |
I offer a variant for discussion. I don't know what I think of it either, so I am neither arguing for or against it at this time. In the same way that const x = using expr; The {
using f(using g());
// ...
} should be equivalent to {
const tmp = using g();
using f(tmp);
// ...
} |
That's pretty interesting. However, I'd argue that it's also more confusing. I like that you have to explicitly write out {
const tmp = using value g();
using value f(tmp);
} because it reminds you exactly what the scope and "lifetime" of the value are. |
Please keep in mind that const temp = using g(); // seems ok...
const temp = using (g()) // calls a function named "using"
const temp = using [g()] // element access on an object named "using"
using f(tmp); // seems ok...
using (f(tmp)); // calls a function named "using"
using [f(tmp)]; // element access on an object named "using" You might consider some kind of restriction to prevent ParenthesizedExpression, but there are valid use cases to allow parens: using (void 0, container).func(); // Oops... I wanted to call `container.func`
// without a `this` and dispose of its result
using (a ?? b) || c; That's why I proposed using value f(tmp); // disposes the result of f(tmp) at end of block
using value (f(tmp)); // disposes the result of f(tmp) at end of block
using value (void 0, container).func(); // does what I expect. I'm not certain I'm in favor of {
f(() => using value g()); // no {} to indicate a block.
} Versus having it be a statement: {
f(() => {
using value g(); // definitely scoped to the block for the arrow.
});
} |
@rbuckton Good point! I hadn't considered expressions in arrow functions without curlies. I agree that this would be too confusing. Another place this comes up: field initialization expressions. Where else do we have effective block boundaries without curlies? |
If i understand your question correctly, if/else/do/while/for braceless blocks? (not sure if you’re only looking for places a statement is allowed and a block is implied) |
Hi @ljharb , good point! I was not thinking of those cases, but rather, cases like arrow functions and initialization expressions where there's an implied block, but only around an expression, not a statement or declaration. If this proposal were for a statement, then the ability to omit curlies in if/else/do/while/for indeed be a similar problem. But since @rbuckton is proposing only declarations, not statements or expressions, his proposal would force curlies in this context as well. This is a big +1 for making it a declaration specifically. Looking through the grammar, I found the following interesting places a declaration could occur: for (const using x = ...; ...; ...) {}
switch (...) { case ...: const using y = ...; ... }
export const using z = ...;
// top level of module using top-level await Is the behavior for all of these clear? The one that seems most worrisome is the switch. |
@erights: A few notes:
Also, the proposed syntax is |
All seem reasonable. Thanks! |
I didn't catch this initially, but this reminds me a ton of an idea I came up with a couple weeks prior to this issue being created. |
question: what happens here? // Example 1
{
using const x = { [Symbol.dispose]() {} }
delete x[Symbol.dispose]
} // Does an error get thrown?
// Example 2
{
using const x = { [Symbol.dispose]() {} }
x[Symbol.dispose] = () => console.log('Hi there')
} // Does "Hi there" get logged? Based on the transpiled examples, I understand that in example 1 an error will not be thrown, and in example 2 "Hi there" will not be logged. What this tells me is that the "using const" causes a single event to happen when a declaration happens (that event is registering an exit handler on the scope), and that the resulting declared variable hasn't been made special or different in any way from a normal const. The only relationship I vote we follow @rbuckton's idea and drop We have options on how to handle the
I personally prefer the first option. I think it's a very simple ruleset to remember - you can only use Some other benefits of
|
I'm not sure where you got that impression. I specifically said this, above:
We've also discussed this in plenary and the consensus was that allowing |
Note: I've updated #56 to disallow |
I'm not actually sure where I got that from - perhaps I just misinterpreted what you said. My vote would still be for At the very least, perhaps it would be nice to actually make the declarations special, like the syntax suggests it's happening. i.e. the engine doesn't look for the Symbol.dispose property on a "using const" declaration until the block is exited, at which point it'll look up a disposal handler and call it. I know in practice the Symbol.dispose property should never be changed, but in principle, I think this behavior helps make the mental model more intuitive, at least it does for me, perhaps others would disagree. |
@theScottyJam I thought that was the original intent of using a declaration-based syntax instead of a Java/C#-like |
From what I understand, when you use Is this what you're asking? Or could you clarify? |
@theScottyJam we have already discussed in plenary when |
There's a lot of discussion on the IRC about making
using
a new binding type:I'll let the others fill in their thoughts.
The text was updated successfully, but these errors were encountered: