Sorry for the utterly confusing title, it's the best I could come up with.
This isn't really a bug in Effection, but rather a consequence of some of the design patterns we've started to adopt. Since I ran into this a few times and it is potentially very confusing, I thought it best to actually document this somewhere so we can try to find a solution to it.
The problem occurrs if we're using the RAII-like pattern of creating some value object which has an implicitly created context associated for it, or an ensure block. For example, we might create an http server (value) with an associated ensure block which closes the server (context), like this:
function *createServer() {
let server = new Server();
yield suspend(ensure(() => server.close());
return server;
}
In some other context we could then use the server like this:
function *main() {
let server = createServer();
yield fork(function*() {
server.doSomething();
});
}
The structured concurrency guarantees ensure that the server won't be closed before any fork in the main
function has completed. So we have effectively bound the server to the scope if was created in, and ensured it will be de-allocated at the end of this scope.
Now let's imagine we want to create a special kind of server, and we want to re-use the createServer
primitive:
function *createSpecialServer() {
let server = yield createServer();
doSomethingSpecial(server);
return server;
}
Now we have a problem! The server returned from createServer
is bound to the scope it was created in, which is the createSpecialServer
scope, and the structured concurrency guarantees ensure that it won't close until that scope is complete.
BUT at the end of the scope we are RETURNING the server, and effectively passing it UP to the parent. But we just noted that the server will close at the end of the scope!
function *createSpecialServer() {
let server = yield createServer();
doSomethingSpecial(server);
return server; // close will run here!!!
}
So what we return from createSpecialServer
is a closed server!
Okay, so we realize that we need to hoist the server up one scope, maybe we can do this:
function *createSpecialServer() {
let server = yield suspend(createServer());
doSomethingSpecial(server);
return server;
}
Unfortunately this doesn't work, since suspend
will return a Context
and not the server, so if we use suspend
we can't actually get the value!
So fundamentally we have a problem in that this pattern simply does not compose, and when it doesn't compose it fails in a surprising, and (as should be evident by this lengthy explanation) pretty difficult to understand way.
As I said this isn't a bug in Effection per se, but it is something we need to think about as we're trying to find recommended practices in how to use Effection.