Iterators, generators, finally, and scarce resources (Was: April 10 2014 Meeting Notes)

K. Gadd kg at luminance.org
Tue Apr 29 21:50:27 PDT 2014


Minor correction to Domenic's comments in this (interesting)
discussion; IEnumerable and IDisposable are separate concepts in C#.
Neither IEnumerable or IEnumerator are disposable objects in C#;
*however*, if you use 'for each' on an object that yields an
enumerator that is *also* disposable, the compiler-generated sugar for
the enumeration will dispose it.

This is an important distinction: As described in the discussion
thread here, not all enumerables and/or enumerators are disposable.
Many of them are not, and have zero cleanup logic, so there's no need
to include or call a Dispose method. That sugar support is explicitly
there for enumerators that, if disposed explicitly, can release
important resources earlier than the garbage collector would (file
handles, sockets, etc.)

On the other side (enumerator/enumerable authoring), C# solves this
rather lazily: You can use try/finally (but not try/catch) around a
'yield' in an enumerator function, allowing you to do basic cleanup -
the cleanup logic is moved into the compiler-generated object's
Dispose method. This means that if you want cleanup, you opt into it
explicitly, and use the same mechanism you use in normal
non-enumerator functions. There's one caveat that this causes
unexpected disposal behavior in some corner cases (due to how C#
initializes generators), but that's more of an issue with their
generator implementation than anything else.

On Tue, Apr 29, 2014 at 12:17 PM, Domenic Denicola
<domenic at domenicdenicola.com> wrote:
> Dave and Andy's responses have me pinging back and forth as to which "side" I'm on. Both seem convincing. Dave's response especially brought the issue into focus for me in a way that I think is clear, so let me explain what I learned from it:
>
> What we are essentially talking about here are two types of things:
>
> - Disposable resources
> - Iterable resources
>
> We are talking about both the sync case, and the async case. Andy's contention is that most sync iterable resources are not disposable, whereas Dave's is that most async resources are disposable. Both of these positions seem plausible to me.
>
> The question then comes, should for-of handle iterable resources only (as it does today), or should it take care of disposable resources as well (as it does in e.g. C#)?
>
> Andy's code example, viz.
>
> ```js
> var file = openFile("foo.txt");
> try {
>   for (var line of lines(file)) {
>     if (line == '-- mark --') {
>       break;
>     }
>   }
> } finally {
>   file.close();
> }
> ```
>
> advocates for this separation, putting the burden on the user to handle the disposable part, letting for-of focus on the iterable aspect. Dave advocates that this code become
>
> ```js
> for (var line of lines(openFile("foo.txt"))) {
>   if (line == '-- mark --') {
>     break;
>   }
> }
> ```
>
> and the disposableness be handled automatically, which is certainly more convenient for the user.
>
> This second code example, however, hides two kinds of magic: iterability, and disposability, in the same syntax.
>
> An alternative would be to introduce a construct specifically to handle disposability, like C#'s `using`. You could use it generically such that `using(x) { ... }` becomes `try { ... } finally { x.dispose(); }`. In particular the example would become
>
> ```js
> using (var file = openFile("foo.txt")) {
>   for (var line of lines(file)) {
>     if (line == '-- mark --') {
>       break;
>     }
>   }
> }
> ```
>
> This still isn't all that convenient, of course. And going along with Dave's argument, it will become especially inconvenient when async iterables, most of which will be disposable, start appearing. Perhaps this is why C# decided to include both iterable and disposable functionality in their `foreach`.
>
> But inconvenience is easily solved via MOAR SUGAR:
>
> ```js
> for (var line using files) {
>   if (line == '-- mark --') {
>     break;
>   }
> }
> ```
>
> I like this approach for a few reasons:
>
> - It decouples iterability and disposability, giving each distinct syntax constructs
> - Via sugar, it composes them into something just as convenient as if we had baked both of them into `for`-`of`, while giving you an explicit signal of what's going on and what the different semantics are.
> - It of course avoids any optimization hazards, being opt-in.
> - Most importantly, it pushes off this question into ES7, when we can properly design a counterpart `using` block to build on top of.
>
> The drawback of this approach is that it doesn't bake in a disposability protocol into the language. By saying that `for`-`of` will invoke `return()`, we are essentially saying "if you want a disposable object, use the method named `return()` to dispose of it. This kind of ecosystem standardization is a good thing. But on the other hand, if in ES6 this disposability protocol is only useful for synchronous iterators---which, as Dave admits, are less likely to represent disposable resources than async ones---then it's unclear that much is gained. I'd rather give the ecosystem another year or so without a standard dispose protocol, if it means we avoid making changes to ES6 this late in the game.
>
> Anyway, regardless of the specifics of my `using` proposal, I hope that highlighting the iterability vs. disposability aspects of this conversation was helpful to people.
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss


More information about the es-discuss mailing list