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.