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
758 stars 30 forks source link

Can `using` be applied to an expression? #210

Closed cowboyd closed 8 months ago

cowboyd commented 9 months ago

I'm thinking of the case where you want to start a resource so that it can be running, but then it gets shut down when it passes out of scope. As such, you're executing it for its side effects, and not to actually use a handle

{
  using await createServer({ port: 8800});
}

I don't need to use the value returned by createServer(). I just need the server running and handling requests within the given scope, and to be disposed when it passes out of that scope.

fisker commented 8 months ago

I have the same need for CanvasRenderingContext2D

class CanvasRenderingContext2DState {
  #context

  constructor(context) {
    this.#context = context
    context.save()
  }

  [Symbol.dispose]() {
    this.#context.restore()
  }
}

{
   using new CanvasRenderingContext2DState(context);

  {
     using new CanvasRenderingContext2DState(context);
  }
}

OR

const saveState = (context) => {
  context.save()
  return {[Symbol.dispose]: () => context.restore()}
}

{
   using saveState(context);

  {
     using saveState(context);
  }
}
rbuckton commented 8 months ago

We previously were considering void bindings as part of this proposal, and the topic has come up in proposals like pattern matching as well. With void bindings, you might write this instead:

{
  using void = saveState(context);
  {
    using void = saveState(context);
  }
}

It's highly likely that void bindings will be proposed as a standalone feature in the future, so we are not including it in this proposal. In the meantime, you would need to use temporary variables (which might require lint rule overrides for some linters):

{
  using _ = saveState(context);
  {
    using _1 = saveState(context);
    using _2 = someOtherUnusedDisposable();
    // etc.
  }
}

For context, void bindings are also being considered for pattern matching as a means to introduce an explicit "discard" pattern that would be consistent in both pattern matching and destructuring:

obj is { x: void, y: void }; // obj must have 'x' and 'y' properties, the values don't matter
const { x: void, y: void } = obj; // reads 'x' and 'y' (possibly only for side effects)
using void = new UniqueLock(mutex); // discards binding for lock since it isn't otherwise referenced
const Point(void, y) = obj; // extractors
const [void, a, b] = ar; // explicit discard marker instead of an elision
fisker commented 8 months ago

Maybe too late to change to

{
  using saveState(context);
}

{
  const handler = using saveState(context);
}

?

fisker commented 8 months ago

With current proposal, maybe an empty deconstructing can also be used

{
  using {} = saveState(context);
}

{
  using [] = saveState(context);
}
rbuckton commented 8 months ago

Maybe too late to change to [...]

This is something we have decided we are strongly opposed to supporting. For await using declarations, it would bury the fact that there is an implicit await at the end of the block by having await using appear at an arbitrary position within an expression. Instead await using must appear as a statement within the block to make it easier to recognize. Since we cannot support it for await using, we would not support it for using as well, so that the syntax remains consistent.

Additionally, using in an expression position would run afoul of ambiguity if the resource you want to track comes from a parenthesized expression as using(foo) is already a legal call expression.

However, if you do need to track a resource inside of another expression, you should be able to use DisposableStack instead:

{
  using stack = new DisposableStack();
  const handler = stack.use(saveState(context));
}

This is essentially the same as what a pure expression-based using might accomplish, but aligns with the requirements for await using:

async function f() {
  {
    await using astack = new AsyncDisposableStack();
    const handler = astack.use(saveState(context));
    ...
  } // implicit `await stack[Symbol.asyncDispose]()`
}
rbuckton commented 8 months ago

With current proposal, maybe an empty deconstructing can also be used [...]

Destructuring is expressly forbidden by using declarations. In addition const {} = x and const void = x differ in that const {} = x requires that x is neither null nor undefined, as neither can be destructured.

rbuckton commented 8 months ago

To follow up to my earlier comment, a proposal to add void Discard Bindings is currently on the docket for the next TC39 plenary.

using void = expr() should address the general case in the OP, so I am closing this issue. If you are interested in additional discussion or follow up, I would suggest opening an issue in the void Discard Bindings proposal repo.

rbuckton commented 7 months ago

Just to close the loop here, Discard/void bindings are now a Stage 1 proposal: https://github.com/tc39/proposal-discard-binding