Although I tend to mostly use async/await these days, I find the other JavaScript asynchronous primitives interesting. Generator functions, iterables, and async versions of the two.
The author shows a plausible way to put these primitives together to create coroutines powering rendering and lifecycles for web components. And although I'm pretty familiar with the syntax involved, I still learned some new ways to put it together from this article.
Also the author's previous intro article to coroutines in JavaScript might be helpful for learning the basic syntax and patterns: https://lorenzofox.dev/posts/coroutine/
What are some use cases of using generators to do coroutines?
I honestly can't think of an example that couldn't be handled in a stateful way, by using objects/class instances instead of closures.
Even iteration can be handled by implementing [Symbol.iterator] on the instance.
The one place I found a use for them is to syncify async functions in some rare cases where it's needed, as the API would only work with other non-sync functions.
Although it doesn't really matter for much I've found closures to run faster than accessing class properties and found classes to use less memory than closures.
Handling user inputs.
Each time the generator yields you wait for a user input, and when you receive it you next() the generator with the input. It’s less verbose than using a bunch of methods, or the closest alternative I saw which is a switch/case on the index of the current operation. By using generators you just reuse the standard structures of the language.
I'm the same camp. I use async/await because they exist and it's usually a good idea to use similar approaches to other devs, but I question why we even needed async/await in the first place. It's a big deal to add syntax to a whole language, and a small one to add a function to a library. We have
There's some unpopular cases like async iterables or async generators, but for the most part we could've done the same thing without extending the language. I remember the v1 of koa that had yield everywhere and people thought it was confusing af. Then they released with await and suddenly it made sense to people.
Interesting take. One could argue that by adding async functions on top of classic and generator ones we now have three different colors for the functions instead of just tw.
Async await is a syntactic sugar for functional continuations. These are “free” for C stacks, in a sense. Coroutines are not, because you have to yield and resume across C calls sometimes. The common argument for async await is that you avoid making system interfaces resumable. Another argument is that await explicitly marks yield-across points and there’s no sudden yield through an incomplete or potentially unsynchronized state.
I’ve tried to get into Streams at one point, for their promise of efficient processing of large (infinite) streams of data. Great for stuff like kafka / event streams.
There is a lot of very nice functionality in Node around it - pipe functions and the like, and it can work, but in the end I’ve settled on just using async iterators.
It’s just so easy and understandable to go
for await (const item of my AsyncIterator) { … }
And for people who don’t grasp in detail how it all works, the abstraction is very easy to use.
If you enjoy this approach, you might enjoy the Crank JS framework. https://crank.js.org/
> Crank uses generator functions to define stateful components. You store state in local variables, and `yield` rather than `return` to keep it around.
I love this for some reason. However, I wonder if it would end up being more complicated than React in practice. I generally prefer pedantic/stricter usage guidelines/conventions.
I started experimenting with this recently. Neither my knowledge nor the maturity is at a level where I would use it for production work but there are definitely some promising aspects to this. I am preparing a talk about it.
The most significant unknown for me is how state management (like Redux etc.) would work in a larger app. I can think of many solutions but I can't figure out whether the tradeoffs then result in a net gain or loss.
This is a common idiom when working with generators, since iteration is controlled on the consuming side. Consider e.g. a trivial generator that counts up from one:
function * makeSequence() {
let i = 1;
while (true) yield i++;
}
This function returns an iterator:
const ints = makeSequence();
And now the caller is control of iteration, not the callee:
function sumTo(cutoff) {
const ints = makeSequence();
let sum = 0;
for (let i = 0; i < cutoff; i++) {
sum += ints.next().value;
}
return sum;
}
In this case there is no risk of an infinite loop.
>This is a common idiom when working with generators, since iteration is controlled on the consuming side.
I realize that, and it's why I (and most) have avoided the syntax entirely. Generators today feel more like a vestigial appendix to the language; an aborted branch of development, than anything I'd ever use or suggest.
I've been half-jokingly advocating for "resumable SSR" components that abuse wasm as an isomorphic, serializable coroutine runtime: start rendering a component inside a wasm VM on the server, while simultaneously serializing and streaming the state of the wasm VM to the client, and then resume rendering on the client.
It's a bit of a meme that takes the idea of "React Server Components" to their logical extreme. And coding a PoC is beyond my capabilities... but I'd love to see someone do it.
Sounds cool but in practice downloading a wasm bundle is bandwidth heavy and rendering to the dom in a browser is different than rendering to a string.
I really enjoyed the article! Generators have been around for maybe half a decade already but I never see them used. I am able to follow the article but have never seen an example at even small scale. All of the complexity for my software is still there in some form or other. I can have 100 classes it seems or 1000 functions and I really really struggle with breaking away from the imperative viewpoint. Especially when you have a small team consisting of seniors to juniors. I do love the pattern but no one else does. Why does it feel this way?
Regarding components, states and encapsulation, also mind the much ignored `Object.prototype.handleEvent`, which has been around since 1997. I, know, it's OO, but it is actually simple:
function Counter(buttonElement) {
this.clicks = 0;
this.handleEvent = function(event) {
switch (event.type) {
case 'click':
this.clicks++;
console.log("I was clicked " + this.clicks + " times." );
break;
//handle any other event types
}
};
buttonElement.addEventListener("click", this, false);
}
const myCounter = new Counter( someButtonElement );
(In case you wouldn't know: if an object is used as an event listener, its `handleEvent()` method will be called with that object bound to `this`. In a way, arrow functions are working around what had been already solved by this mechanism.)
In the context of coroutines, the nice thing about this is that – as a catch-all trap – it works much like an interrupt that is internal to that specific object. (Of course, only when the GUI thread allows for event processing.) On the one hand, this is pretty much the essence of what events should be about: invoking a dormant object (or context) by the means of a message. On the other hand, this inherent locality may pose a problem for any framework. (And – in the age of AI generated images – on the third hand, this is why I personally don't like frameworks that much.)
It doesn’t disallow it, it’s just outside of how the framework operates. If you really want to make the two talk, you could, with the cost of having to maintain it.
I'm not entirely sure what this adds in this case, though. For example, in the code you've written, as far as I can tell there's no advantage compared to defining the function as a closure variable and using it directly, rather than defining it as an attribute on `this` and passing the `this` object around.
In addition, with the `handleEvent` approach, you need to handle all events within a single function. But it's fairly easy to create multiple functions within a single function scope and pass them to different event handlers, thus avoiding the need for the large (and potentially error-prone) switch statement if you end up needing to handle lots of events.
Have you found cases where `handleEvent` works better than just defining local variables within a function and just using those? It seems to me that you wouldn't even need arrow functions to take advantage of the natural power of closures in this context.
One of the advantages is that this can be defined and handled on the prototype level and not just in the instance. Personally, I'd see the interrupt-like catch-all behavior rather as an advantage (all messages are received in a single slot), but this may be up to personal taste. And, while it's true that similar binding can be achieved using closures, this has also been true for arrow functions. It's just a convenient solution in the mindset of the original ECMA Script (ECMA 262-2).
Regarding prototypes, mind how this shares methods between instances, rather than consuming (and locking) resources by individual closures created in each of the instances (which, when GC was still based on reference count, would also have meant memory leaks):
38 comments
[ 2.6 ms ] story [ 86.8 ms ] threadThe author shows a plausible way to put these primitives together to create coroutines powering rendering and lifecycles for web components. And although I'm pretty familiar with the syntax involved, I still learned some new ways to put it together from this article.
Also the author's previous intro article to coroutines in JavaScript might be helpful for learning the basic syntax and patterns: https://lorenzofox.dev/posts/coroutine/
I honestly can't think of an example that couldn't be handled in a stateful way, by using objects/class instances instead of closures.
Even iteration can be handled by implementing [Symbol.iterator] on the instance.
The one place I found a use for them is to syncify async functions in some rare cases where it's needed, as the API would only work with other non-sync functions.
There is a lot of very nice functionality in Node around it - pipe functions and the like, and it can work, but in the end I’ve settled on just using async iterators.
It’s just so easy and understandable to go
And for people who don’t grasp in detail how it all works, the abstraction is very easy to use.> Crank uses generator functions to define stateful components. You store state in local variables, and `yield` rather than `return` to keep it around.
The most significant unknown for me is how state management (like Redux etc.) would work in a larger app. I can think of many solutions but I can't figure out whether the tradeoffs then result in a net gain or loss.
I realize that, and it's why I (and most) have avoided the syntax entirely. Generators today feel more like a vestigial appendix to the language; an aborted branch of development, than anything I'd ever use or suggest.
Is this a good thing?
It's a bit of a meme that takes the idea of "React Server Components" to their logical extreme. And coding a PoC is beyond my capabilities... but I'd love to see someone do it.
In addition, with the `handleEvent` approach, you need to handle all events within a single function. But it's fairly easy to create multiple functions within a single function scope and pass them to different event handlers, thus avoiding the need for the large (and potentially error-prone) switch statement if you end up needing to handle lots of events.
Have you found cases where `handleEvent` works better than just defining local variables within a function and just using those? It seems to me that you wouldn't even need arrow functions to take advantage of the natural power of closures in this context.
Regarding prototypes, mind how this shares methods between instances, rather than consuming (and locking) resources by individual closures created in each of the instances (which, when GC was still based on reference count, would also have meant memory leaks):
I wrote my own ui/effects barebones runtime on it a few years back https://github.com/marcellerusu/capable-js
It’s especially cool when you have components that have distinct states they could be in, eg. a survey