-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a schematic state machine implementing Future #2048
base: main
Are you sure you want to change the base?
Conversation
# Recursion | ||
|
||
An async function's future type _contains_ the futures for all functions it | ||
calls. This means a recursive async functions are not allowed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would clarify this by invoking the analogy with recursive enums: they need an indirection to avoid being infinite-sized types. Prior to Rust 1.77, recursion in async fn
was forbidden entirely and code had to forgo the async fn
transform in favor of explicitly returning a Box<impl Future<Output=...>>
type, but now only "bare" recursion (without an indirection) is forbidden.
- All local variables are stored in the function's future struct, including an | ||
enum to identify where execution is currently suspended. The real generated | ||
state machine would not initialize `i` to 0. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes more sense for explanation to first trace the control flow so we can enumerate states and get in our heads something like "we'll have three possible execution states for this async fn, so it makes sense that its future type would be (morally) an enum with three variants". Then we can look at variable liveness at each of the awaits to determine the payload of each variant. This lets us get to "futures store live locals" without having to introduce the notion of liveness explicitly like a compilers course would--these are just the variables whose values we'll need to run the rest of the function.
I think it's better for an example of the state machine transform to use an async fn that doesn't have async in a loop, so that it's easy for readers to enumerate the entire set of states in their head.
So I might suggest an example more like this:
async fn send(s: String) -> usize {
println!("{}", s);
s.len()
}
async fn example(x: i32) -> usize {
let double_x = x*2;
let mut bytes_written = send(format!("x = {x}")).await;
bytes_written += send(format!("double_x = {double_x}")).await;
bytes_written
}
This gives us three states:
- an initial state holding the argument
x
- a state holding
double_x
until we return from our firstawait
- a state holding
bytes_written
until we return from our secondawait
This is, I think, less confusing than a state we return to across iterations of our loop on 1..=count
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implicit in this suggestion is to use enum variants to represent the liveness of variables. That's great, but does require a bit more mucking about with pins than we want to present at this point. In fact, it uses pin_project
, which we don't even talk about in the Pin section. Should I maybe revert this to a flat struct with a fut
field, just to avoid this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's so important to make this example have perfect fidelity or be compilable/runnable--pseudocode to capture the transformation would be fine, and would allow us to side-step Pin
questions. The sub-future, if we really want to represent it, could just be a std::future::Ready
or isomorphic. The most significant thing is not the state of the child future but the fact that at await points we capture live state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments inline.
Reading through https://blog.yoshuawuyts.com/self-referential-types/ I see links to https://tmandry.gitlab.io/blog/posts/optimizing-await-1/ and https://tmandry.gitlab.io/blog/posts/optimizing-await-2/, all of which might help with this. |
@fw-immunant generated something like this on-the-fly in teaching the course, and I thought it was great. I think having a schematic understanding of what's going on here helps students through some of the pitfalls. Particularly, it motivates
Pin
, which is where @fw-immunant did this derivation.