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
776 stars 29 forks source link

Combining Iterables/AsyncIterables with Disposable/AsyncDisposables #241

Closed philipnilsson closed 1 month ago

philipnilsson commented 2 months ago

I'm using this proposal via the implementation currently available in TypeScript, and a pattern I've found myself often wanting to use is to have AsyncIterables that are also Disposable/AsyncDisposable. An example could be a function that asynchronously yields the contents of a file, line by line. This would of course also need to dispose of the open file at completion. I'm currently writing this as

using lineReader = readlines('myFile.txt');
for await (const line of lineReader) {
  //
}

where lineReader has implementations of both a [Symbol.asyncIterator] and a [Symbol.disposable] (@@asyncIterator / @@dispose in your terminology?)

I'm wondering if there's a better way of doing this. My concern is that the first line is a bit redundant, and easy to forget. Ideally I'd love to just write something along the lines of

for await using (const line of lineReader) {
  //
}

I see there is syntax in this proposal allowing the using statement inside the loop expression, which is fine but seems very rare in comparison to wanting the resource management to be tied to the expression producing the async-iterable. Am I missing something here, or would this be recommended usage?

There also seems to be an opportunity here in allowing the cleanup of the resource immediately after the for-loops completion, which isn't the case in my first example. Of course you could wrap that in another block, but that further reduces the ergonomics.

bakkot commented 2 months ago

Async iterators already have a cleanup method in the form of .return, which is called whenever a for-of early exits. So if readLines has an appropriate implementation (i.e., it returns an iterable iterator which cleans itself up on completion and when its return method is called), then

for await (const line of readlines('myFile.txt')) {
  //
}

should do the right thing already, even without this proposal.

rbuckton commented 2 months ago

Correct. In addition, per the spec the Symbol.dispose/Symbol.asyncDispose methods of built-in iterators/generators invoke return, so the using is redundant in this case. The main reason to use using with iterators is to help with manual iteration, i.e.:

using iter = readlines('myfile.txt');
for (let res = iter.next(); !res.done; res = iter.next()) {
  ...
}