Composing Generators

If I define a function f that returns a value with a dispose handler:

f = value => Generators.disposable(value, () => console.log("inner cleanup"))

And I then use this value in another cell, which adds its own dispose handler:

{
  let value = 100;
  let ret = f(value);
  return Generators.disposable(ret, () => console.log("outer cleanup"));
}

The result of the second cell is the generator returned by f, rather than value.

Is there a way to allow multiple dispose handlers at different levels of an implementation to cleanly compose together?

I’m exploring the underlying behavior in this playground notebook: https://observablehq.com/d/3473300e593a4000

Thanks!

Maybe the answer here should look something like a Generators.flatMap or Generators.flatten. It doesn’t have to live in the stdlib, of course.

One approach is here: https://observablehq.com/@bobkerns/livedata

Sorry for the poor state of documentation and comments; I just did a rushed rework before publishing it; I’ll try to clean it up in the next few days.

The basic idea is to wrap a function of n arguments to get a new function that takes n generators (or promises of same), returning a new async generator. Then you use this to arrange to always return a generator to the top level, so it can handle invalidation. (You could use invalidation.then(() => gen.return()) of course, but I haven’t thought of a case where that would be preferable to just letting the top level handle it.

Every time next() is called on the resulting generator, the function is called on the (resolved) next() values on the generators.

The underlying mechanism gives options for terminating on the first generator’s completion, or letting finished ones become undefined, or to fill in with the last value seen until all have terminated.

This is particularly useful when supplying some constant values, as they become generators of that constant value.

If you invoke return() or throw() on the composite generator, the same is done to all of the input generators. (Yes, including throw, which is done on each before the composite one itself throws).

It needs more testing, and obviously, documentation, but I thought I’d put it out for discussion since it’s a current topic.

I’d also like a version that calls the function whenever any of the input generators yields a new value.

4 Likes

Hi Bob,

Thanks for your answer and notebook! This looks very interesting. I’m going to have a close look at it when I have the bandwidth.

This is a challenge.

The reason that disposable generators don’t compose is that Observable uses generator.return to trigger disposal, rather than the “natural” end of the generator. And that’s because you don’t want to trigger disposal immediately when a generator is done—you only want to trigger it when the cell is invalidated.

The invalidation promise is a more explicit way of running code when a cell is invalidated. The challenge with that is you have to pass invalidation in to the disposable object you’re creating, which can be tedious (as opposed to Generators.disposable where it can happen implicitly, but losing composability).

Maybe wrap Generators.disposable to create a (proxy) invalidation promise, and then pass that promise to whatever else you want to dispose? It’d be easier to give a more concrete suggestion if you shared more details about what you’re trying to achieve.