What about async/await? You can basically write linear, imperative code, and don't have to deal with continuations or these "tuned structures" manually.
For me it is the cleanest style of writing concurrent code. And more and more I find I can also replace state machines with it, which makes sense because the compiler generates state machines under the hood usually.
You know, the kind of code where you have to communicate with some outside device and it is easy to do blockingly but devolves to state machine madness if you need to do other things concurrently. For example it would be really nice if I could use async/await in C on a microcontroller to read from a serial port...
2. Poor interaction with debuggers, profilers, and other tools that expect to be working with normal stacks.
Loom solves this because it lets you work with normal threads, but suddenly you can have millions of them in a process without blowing out your memory or other related problems.
> You know, the kind of code where you have to communicate with some outside device and it is easy to do blockingly but devolves to state machine madness if you need to do other things concurrently.
Speaking personally, I've found Lua's coroutines to have the nicest experience for modeling flows like that. The big issue with async/await is the function color problem [0] -- writing async functions is perfectly fine, but mixing them with non-async functions can be extremely frustrating. Especially if you're doing anything with higher-order functions.
I used to struggle with "function color" until I realized that the functions just have a different type. Async functions return a future `Task<Thing>`, while normal function return a plain `Thing`. Of course they are incompatible.
A different way of looking at it is that in asyncs functions you should only do things that have negligible runtime (compared to the response time of your GUI or network service). If your task needs more time, you mark the call site and the called function "async" and the task will suspend somewhere "down in the call stack". (Without looking into it too much, I think something similar actually happens with these virtual threads. They modified IO functions to do cooperative multitasking under the hood?)
As to async functions being contagious, I found it helps to split "imperative" procedures and "pure" functions, and the async color mostly applies to the previous.
The problem with function coloring is that it divides the language for no good reason. Should there really be two names for the same sleep function, just because one is blocking and the other is not? As for a return type, that’s just a leaky abstraction imo (especially for voids, like is a blocking call returning nothing different than an async call returning a Future void?)
As for loom, due to it running all in a runtime, a blocking ‘read’ call for example is not actually a blocking system call (everything uses non-blocking APIs at that level) so the runtime is free to suspend execution at such a blocking site and continue useful work elsewhere until that finishes. So for some “async” functionality you can just fire up a new virtual task with easy to understand blocking calls and that’s it, it will do the right thing automagically, and it will throw exception where it actually make sense, you will be able to debug it line by line, no callback hell, etc.
Loom will also provide something called structured concurrency where you can fire up semantically related threads and easily wait for their finish at one place.
As for pureness, I don’t think it maps that cleanly to async/blocking. What about doing the same function on each pixel of a picture in memory where you subdivide it into n smaller chunks and run it in parallel?
Personally, in JavaScript I like that you can mix and match imperative and asynchronous code using Promise instances. It lets you handle asynchronous control flow in a purely synchronous function.
However in other languages, having functions be of a different 'color' is far more painful. In Python for example, a synchronous function has to setup an event loop manually before it can run an asynchronous function. The call works, but nothing is 'running' without the event loop. Additionally, the asynchronous function may have been written to work with a particular eventloop (e.g. trio vs curio), and thus you have to use that type.
If non-blocking code has a standardized control state like Javascript, I think it's better to be explicit about async vs sync.
I also think of "color" fundamentally as different types; it's just painful to have two kinds of functions that you can't combine. I, personally, really feel that async/await is just adding a second kind of continuation (promises/futures) to a language that already has a perfectly good one (call stacks), and the language ergonomics suffers for it.
The reason I say functional languages don't get bit by this as bad is because functional languages rely far less on the specific notion of a call stack, and it's usually much easier to work with continuations (either via primitives like shift/reset or via syntax like do-notation).
I struggle with this idea of "separate colors"... I see Promise returning functions as a super set of immediately returning functions... that is, any function that can return immediately could also return as a Promise (which resolved immediately), so really, the immediately returning function is just an optimization to apply when it's helpful. I'm curious why a language suffers from this explicit separation of code which "returns immediately" versus code which "returns eventually"?
For me it is the cleanest style of writing concurrent code. And more and more I find I can also replace state machines with it, which makes sense because the compiler generates state machines under the hood usually.
You know, the kind of code where you have to communicate with some outside device and it is easy to do blockingly but devolves to state machine madness if you need to do other things concurrently. For example it would be really nice if I could use async/await in C on a microcontroller to read from a serial port...