Hmm. What would Yato's second example look like with manually boxed futures? I don't know how to write that myself, but based on experience with manual futures in JavaScript I'm worried that the actual logic will get lost in the mechanics, even for something very simple like that.
Note that no Rust language feature implicitly allocates at the moment, so this would be a significant change.
"You must have an allocator if you call and await this function at this point" also seems like a fairly odd consequence to impose on users, especially because it can happen non-locally at any arbitrary point you create a cycle in the control flow graph.
I don't really understand the problems with manually boxing, since the compiler even tells you to do it when this happens. Does this cause issues for anyone here?
Well, like I said upthread, my concern is with readability. What does a moderately complex recursive async function look like with manual boxing where necessary? Can you point me at some examples?
Even though there's no magic solution, the fact remains that recursive async functions are cumbersome, and the compiler is not helpful about them.
Currently the compiler just points to function's return type, and leaves it to the user to guess what the right solution is. The rewritten form is kinda annoying to write.
So I would really like to see some solution to this. For example, if std had a macro for #[boxed_async] transformation, then the compiler could recommend it automatically. It would downgrade the problem from tricky situation with boilerplate solution to a 1-line autofix.
Compiler could make error more readable, something like:
It is not possible to make recursive call without level of indirection !!
Please, use boxed_async macro or run async function in sheduler
that is supports impl RecursiveFuture
Have you seen my message above ?
I've already implemented custom macros #[async_recursive] and today at the morning already renamed it to #[boxed_async_recursion] ))
Because the outer-most call gets boxed unnecessarily. recursive doesn't need to return a boxed future, it's only the recursive calls to recursive inside recursive that need to be boxed. Using box explicitly where you want to break the cycles in the control flow graph is more efficient. #[boxed_async_recursion] can't do this if the recursion happens across multiple functions.
Admittedly this is a micro-optimization so maybe something like #[boxed_async_recursion] would still make sense to have in the standard library. I think better error messages that suggest where to put box would be preferable though.
@RustyYato Also seems like it is possible to use alloca-like function to implement recursive async/.await functions:
pub trait RecursiveFuture {
/// The type of value produced on completion.
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
fn create_self<'a>(self) -> Pin<&'a mut Self> where Self: Sized {
unimplemented!("Using alloca-like from C library by default !!");
}
fn dispose_self<'b>(self: Pin<&'b mut Self>) where Self: Sized {
unimplemented!("Dispose object created by alloca-like with dealloca function !!");
}
}
You can't use stack space to store the future, this includes alloca. But this doesn't contradict anything that I said, because you still have indirection, just now it's through a reference instead of a box
I mean what you wrote - with recursive and another_recursive calling each other recursively - but what I'm saying is that there's no way to do this with a macro without doing unnecessary boxing.
Consider these two functions:
async fn not_recursive(x: u8) -> u8 {
x
}
#[boxed_async_recursion]
async fn also_not_recursive(x: u8) -> u8 {
x
}
Calling not_recursive is zero-cost but calling also_not_recursive isn't since it allocates on the heap. So #[boxed_async_recursion] is adding unnecessary cost.
This is also true if you have a function which is actually recursive since only the recursive calls to the function need to be boxed but #[boxed_async_recursion] will box the initial call to the function as well. You could try to modify the macro to be smarter about this by moving the call to .boxed() inside to where the recursion happens instead of wrapping the entire function body in .boxed(). But this won't work if you have mutually-recursive functions (or even otherwise, since macros only operate on syntax and can't necessarily see all the recursion points).
Please, take a look at RecursiveFuture in Playground, it is possible to have Zero-Cost Abstraction even with recursive calls with alloca:
pub trait RecursiveFuture {
/// The type of value produced on completion.
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
fn create_self<'a>(self) -> Pin<&'a mut Self> where Self: Sized {
unimplemented!("Using alloca-like from C library by default !!");
}
fn dispose_self<'b>(self: Pin<&'b mut Self>) where Self: Sized {
unimplemented!("Dispose object created by alloca-like with dealloca function !!");
}
}