I’d really like to align zio with the future std Io, but I have a problem with the terminology.
There is io.async and then there is io.concurrent, both might spawn the function in some kind of background worker, but io.async might not. I understand the rationale, you want to support single-threaded blocking backend where io.async is a direct function call. However, when would you actually want to use io.async? If you are fine with the code executing serially, why would you not just call it as a function? I can’t imagine a single use case for that.
I’d much prefer if we have io.spawn that explicitly runs the code in some kind of background executor. You could see that as gevent in Python, where without gevent, spawn would run in a thread, with gevent, it would run in a coroutine.
There is also the need for io.spawnBlocking, for functions that are not cooperative, but that could be a backend-specific extension.
However, my main question is, what’s one good use case for io.async?
If the program runs in single-threaded blocking mode async is forced to just call the function, however otherwise you still want to run the asynchronous code asyncronously. (not everyone wants to run their program in blocking mode and the code shouldn’t need to be changed to run it in non-blocking mode)
If you always just use function calls (and this code is part of a library) then the user doesn’t have the option to run it asynchronous.
io.async allows that, but if there is no possibility to have a background executor, you can still run the code in single threaded blocking mode, which is slower but enables more use cases. Giving more options to the user.
io.async enables implementation agnostic expression of the actual asyncrony and concurrency (via io.asyncConcurrent), allowing users to write the code once and use it with multiple different io implementations.
When it would be nice to run a function in a concurrent task, but it’s not necessary for correctness. Note also that io.async cannot fail, while io.concurrent can, and you’ll have to handle the failure.
Let’s say I have a library that handles a lot of data, and multiple streams too. It has a higher level API for the most common use case, which is where I will focus, as an example of async in the library.
It’s perfectly fine to process streams one at a time, but it would be a lot faster to do them concurrently.
io.async expresses exactly that, it allows the caller to choose, via the Io implementation, if the processing is concurrent or not, and even the model of concurrency used.
io.async can decide to allocate a unit of concurrency (thread, green thread, coroutine) to run the provided function concurrently or just call the function immediately (e.g. if resource limits were hit).
io.concurrent instead has to guarantee that the provided function will be placed in a different thread / green thread / coroutine.
io.async can be implemented basically as return io.concurrent(f, args) catch f(args); (slightly simplified)