It's still using callbacks, though. So while there may be a single allocation, code conciseness suffers.
Using Rust futures' and_then, or_else, etc interfaces, try writing a parser that consumes data byte-by-byte, where reading each input byte might block. (Similar to using fgetc() in a C state machine.) It won't be very easy to understand, especially compared to threaded code.[1] Doing that with coroutines, even stackless coroutines, would be much more clear.
That's the problem with futures. If futures were so great, we'd use them for all control flow. There's a reason we don't, just like there's a reason we don't use purely functional languages everywhere. But at least purely functional languages are consistent, and usually have other features (like lexical binding and garbage collection) to make alternative control flow patterns easier and more concise.
Ask yourself this: if resizeable, relocatable stacks were a freebie (i.e. already provided, or because there was 0 desire to support a pre-existing calling convention and most of the instrumentation needed for moving stacks was preordained by exceptions), would you ever not implement stackful coroutines? The future pattern API could be easily implemented as a library for when it made sense. Decorating calls with "async" would become superflous, but people would still be free to do that. Message passing and consumer/producer inversions would become free and natural coding patterns. Stackful coroutines are clearly the more powerful abstraction. But implementing them is difficult, especially in the context of legacy environments (e.g. C runtimes) and so people compromise and often choose a less powerful abstraction.
[1] In reality you're likely to use a mismash of patterns and language features because futures will just be too intrusive of a construct for fine-grained control flow manipulation. All of that adds to the complexity of a language and to the solutions you implement in the language.
Back when C10K was first written, the big debate was between threaded vs asynchronous (implicitly callback based) code. In terms of performance asynchronous won the day, but I think the threading proponents were always correct that threaded code was more natural, more clear, and less buggy (ignoring preemptive multithreading). Even though I've spent over 15 years writing asynchronous I/O networking code, I think the threading proponents were correct. For control flow, threading is the best default model and should be the gold standard. Stackful coroutines are similar to threads in terms of how they relate to normal calling conventions and function types. When it comes to the theoretical code conciseness and performance possibilities that's no coincidence.
Once you have callbacks, it's not hard to wrap it in some nice syntax sugar that makes it look like regular code, more or less. Or did you mean something else?
Yes, I agree that it's a matter of "legacy environments". But the thing about it is that they will remain non-legacy for as long as the purported successor is not determined. The C model that everyone currently uses as the lowest common denominator persists precisely because of that - it's what everyone has been able to agree on so far. Beyond that, not so much. So Go-style coroutines may well be awesome, but it doesn't matter because of all the other languages that have incompatible models.
Futures, on the other hand, work for this purpose, because you can represent them even on C ABI level. Granted, it's a fairly expensive abstraction then, but it works. Look at WinRT, where futures are pervasive in platform APIs and code that consumes them, and it all works across .NET, C++ and JS, with a single future ABI underneath all three.
> Ask yourself this: if resizeable, relocatable stacks were a freebie (i.e. already provided, or because there was 0 desire to support a pre-existing calling convention and most of the instrumentation needed for moving stacks was preordained by exceptions), would you ever not implement stackful coroutines?
Don't stackful coroutines necessarily mean you need a runtime (with a GC) that is able to grow the stack ? Or am I missing something ?
To allow for stack growing, the compiler would insert a small number of additional instructions into function entries that either do nothing or call out to a runtime library function that handles stack growing.
Nothing conceptually new in here, that isn't already done by compilers to deal with exception handling, initialize variables or perform division on machines that don't do it natively.
Sure, it requires some runtime support but so does almost every C or C++ program in existence.
FWIW, Rust used to use split stacks, as implemented by LLVM, but they had problems like unpredictable performance (if one happened happened to be unlucky and end up repeatedly calling functions across the split boundary, each of those calls ends up doing far more work than desireable). Relocating stacks is a whole other issue, and almost impossible in the model of languages like C/C++/Rust where there's no precise tracking of pointer locations: the information in exception handlers (approximately) tells you were things are, but nothing about where pointers to those things are.
Yes, copying stacks are unsuitable for unrestricted C or C++ code. Segmented stacks might have problems (Go also switched away from them for the same hard-to-predict performance reason) but when you can't do stack moving I still think they're attractive.
Using Rust futures' and_then, or_else, etc interfaces, try writing a parser that consumes data byte-by-byte, where reading each input byte might block. (Similar to using fgetc() in a C state machine.) It won't be very easy to understand, especially compared to threaded code.[1] Doing that with coroutines, even stackless coroutines, would be much more clear.
That's the problem with futures. If futures were so great, we'd use them for all control flow. There's a reason we don't, just like there's a reason we don't use purely functional languages everywhere. But at least purely functional languages are consistent, and usually have other features (like lexical binding and garbage collection) to make alternative control flow patterns easier and more concise.
Ask yourself this: if resizeable, relocatable stacks were a freebie (i.e. already provided, or because there was 0 desire to support a pre-existing calling convention and most of the instrumentation needed for moving stacks was preordained by exceptions), would you ever not implement stackful coroutines? The future pattern API could be easily implemented as a library for when it made sense. Decorating calls with "async" would become superflous, but people would still be free to do that. Message passing and consumer/producer inversions would become free and natural coding patterns. Stackful coroutines are clearly the more powerful abstraction. But implementing them is difficult, especially in the context of legacy environments (e.g. C runtimes) and so people compromise and often choose a less powerful abstraction.
[1] In reality you're likely to use a mismash of patterns and language features because futures will just be too intrusive of a construct for fine-grained control flow manipulation. All of that adds to the complexity of a language and to the solutions you implement in the language.
Back when C10K was first written, the big debate was between threaded vs asynchronous (implicitly callback based) code. In terms of performance asynchronous won the day, but I think the threading proponents were always correct that threaded code was more natural, more clear, and less buggy (ignoring preemptive multithreading). Even though I've spent over 15 years writing asynchronous I/O networking code, I think the threading proponents were correct. For control flow, threading is the best default model and should be the gold standard. Stackful coroutines are similar to threads in terms of how they relate to normal calling conventions and function types. When it comes to the theoretical code conciseness and performance possibilities that's no coincidence.