One area in which async/await significantly improves over manual futures is borrows between individual futures in the chain. In languages with a GC, this isn't an issue, but it comes up in Rust a bunch.
See this for a great explanation: http://aturon.github.io/2018/04/24/async-borrowing/
Thanks for your work btw.
The problem with stackless concerns code reuseability. With stackless you have to duplicate all your intermediate functions: once for code that calls functions or methods which can yield, and again for code that takes functions or methods which won't yield. Or you simply don't reuse code at all, bifurcating the entire ecosystem.
The stackful vs stackless effectively refers to whether the implementation uses the same stack discipline for calls that can yield vs those that cannot. In either case you're always going to have to construct some kind of stack to support nested function invocation, the question is whether you're going to duplicate all that infrastructure.
Also, it helps if you don't conflate asynchronous I/O with coroutines. Coroutines are a meta abstraction over functions (an abstraction over call chains) that can be used to create ergonomic async I/O, but have other uses, like inverting producer/consumer caller/callee relationships (e.g. converting a push parser into a pull parser with a couple of lines of wrapper code). Stackful coroutines reuse the normal call stack discipline; stackless coroutines require function annotations and compiler rewriting and lead to the code reuse problems mentioned above.
Stackful coroutines are actually a perfect fit for Rust's ownership model, and would simplify much of the work, or obviate it altogether. But for other reasons--C compatibility, poor OS constructs for minimizing memory use, and an unfortunate early conflation of coroutines with async I/O--Rust has chosen the path of stackless coroutines a la async/await as the official model.
It's easy for beginners in the way GOTO spaghetti comes easy to beginners. That's all they know, after all. It however leads to bad designs and complex program flow.
let line = io::read_until(reader, b'\n', Vec::new());
let line = line.and_then(|(reader, vec)| {
if vec.len() == 0 {
Err(Error::new(ErrorKind::BrokenPipe, "broken pipe"))
} else {
Ok((reader, vec))
}
});
// Convert the bytes we read into a string, and then send that
// string to all other connected clients.
let line = line.map(|(reader, vec)| {
(reader, String::from_utf8(vec))
});
...
With monads we end up with much boilerplate that has nothing to do with the actual "business" logic.Maybe you prefer that, but I prefer the code to describe just the business logic.
let (reader, vec) = await { io.read_until(reader, b'\n', Vec::new() }?;
if vec.len() == 0 {
return Err(Error::new(ErrorKind::BrokenPipe, "broken pipe"));
}
let string = String::from_utf8(vec)?;
I myself learned to program on node.js 6 years ago, when everything required piles of callbacks (the least ergonomic form of async/await style concurrency). It's strange to see a bunch of experienced programmers saying that nobody could possibly use this style, as it is too hard, while millions of beginning programmers master it easily. In fact I remember that the people who usually had a problem with callbacks were those who had prior experience with synchronous languages.
Async/await reveals the beauty of this pattern of async, by getting rid of the syntactical cruft of promises and callbacks. It's already awesome in C# and JS, and will be landing in Rust this year.
IMO using "thread" patterns should only be done when you actually have something to gain, with real multi core parallelism. The big advantage of coroutines is that they scale seamlessly across cores. By putting the bad ergonomics of threads everywhere, you make your code ready to scale onto actual threads.