[Post-RFC] Stackless Coroutines

Thanks for the thoughts @annie! One thing I’d be very curious to hear about is whether the futures-await crate as-is lives up to your expectations of what async/await in Rust would look like. I’m always worried that the caveats listed in the README are quite large (especially the borrowing one) and can limit the impact of having a full-fledged async/await implementation. I’ve personally not found them too limiting (modulo the error messages which should get better with compiler support) so I think the implementation as-is with caveats can have a huge impact, but always good to get others’ opinions!

For me at least knowing to the degree that the current implementation (even with its known limitations) solves existing problems is super helpful, namely it helps in prioritizing work and stabilization to understand which subset has the most impact.

I agree with @withoutboats that it’d be awesome to see if we could stabilize some things before others to get this out to stable, but I haven’t personally gotten that far in my thinking process yet, I’m just hoping to get it into nightly right now :slight_smile:

2 Likes

I am very excited to see this land and to see experimentation! My one concern is that I really want to avoid a scenario where we wind up with a push to stabilize without a clear RFC and ultimate specification. This seems like a good place to use a more aggressive experimentation process of the kind we’ve talked about, and I think a key part of that process has to be that we are working towards a “proper” RFC (in fact, for most big RFCs I think we wind up doing a lot of impl work up front anyway, it just doesn’t always occur on master behind a feature-gate).

To that end, I propose a few things:

  • let’s make a big effort to make a comprehensive test-suite with good coverage of corner cases and so forth
    • the tests should be grouped into src/test/{run-pass,compile-fail,ui}/generators/
    • I would include for example the “bad error messages” that @alexcrichton referred to as corner cases; I’d love to see ui tests for those scenarios so that we can track and try to improve them
  • I think the ideal would be if there were people interested in working on the RFC in parallel, and trying to keep it up to date as things evolve (perhaps at some lag, but not too much)
3 Likes

I’m super keen on helping out fleshing this out and contributing to an RFC. Though I’m not exactly sure how to go about doing that, and how it’s coordinated. It seems that a lot of discussion takes place outside of GitHub and the discussion forum, it’s always felt a little inaccessible and hard approach Rust Internals when you are not already involved.

[quote=“alexcrichton, post:14, topic:5021”] One thing I’d be very curious to hear about is whether the futures-await crate as-is lives up to your expectations of what async/await in Rust would look like. I’m always worried that the caveats listed in the README are quite large (especially the borrowing one) and can limit the impact of having a full-fledged async/await implementation.[/quote]

I think it’s a great initial demo. I spent a bit of time on it last night and I have to say that I absolutely love it! Even in its current incarnation, it’s a huge improvement over writing futures-based code.

Personally, I don’t think the current set of limitations hinder the exploration of an await implementation. They’re unfortunate, but they’re easy to work around. We will need cross-suspend-point borrows and self borrows before people start using it, but I’m glad that we have an implementation at all right now.

When self borrows are required, I’m using the fn(this: Box<Self>|Rc<Self>) pattern, which is exactly the same pattern that’s required for a lot of futures code.

I’m going to spend next week porting over an experimental GRPC service over to this. It was previously using a modified compiler that hackily that transformed await-like code into an (unsafe) state machine, so I think it should be more or less straightforward.

6 Likes

I meant making it available on nightly without making generators available. Perhaps I'm being overly conservative, but making any API for generators available on nightly obligates us to it somewhat because people will start using it; I want to avoid even making that commitment to any particular generator API. Async/await on the other hand is a much smaller space of possibilities.

Basically, I'd like to see if we could make async/await available on nightly ASAP, so casting off every commitment we possibly can seems best to me.

Yeah I definitely sympathize with this feeling, and I'm sorry it feels this difficult to help contribute here! We pride ourselves on making Rust as easy as possible to contribute, and this needs to encompass everything including design as well!

I myself am at a bit of a loss of how to best make progress here, I was hoping this discussion could reignite interest and push the discussion along to a conclusion :). In that sense I think the best way to contribute right now is to help provide data on answering open design questions. For example the question of "to be usable does this need borrowing-across-yield-points?" seems to be "no", but confirmations of that are always good! Other more low-level questions about the generator implementation itself I'm hoping @Zoxc or @vadimcn can chime in with to help drive discussion here.

Oh yeah so this is actually something else I ran into pretty quickly. If you've got a trait like:

trait MyStuff {
    fn do_async_task(??self??) -> Box<Future<...>>;
}

It's actually quite difficult to use this! Right now there's a bunch of caveats:

  • Ideally you want to tag this #[async] but this is (a) not implemented in the procedural macro right now (it doesn't rewrite trait function declarations) but also (b) it doesn't work because a trait function returning impl Future is not implemented in the compiler today. I'm told that this will eventually work, though!
  • Ok so then the next best thing is #[async(boxed)] to return a boxed trait object instead of impl Future for the meantime. This still isn't actually implemented in the futures-await implementation of #[async] (it doesn't rewrite trait functions) but it's plausible!
  • But now this brings us to the handling of self. Because of the limitations of #[async] today we only have two options, self and self: Box<Self>. The former is unfortunately not object safe (now we can't use virtual dispatch with this trait) and the latter is typically wasteful (every invocation now requires a fresh allocation). Ideally self: Rc<Self> is exactly what we want here! But unfortunately this isn't implemented in the compiler :frowning:

I think this is actually something I'll add to the caveats section of the README, it seems like if you're using traits returning futures you'll run into this very quickly and unfortunately there's no great answer today. In sccache I got lucky because the trait didn't need to be object safe, so I just used self and cloned futures a bunch.

2 Likes

I just want to leave my opinion here. I’ve seen the fn* syntax floating around to denote that a function is a coroutine/generator. I was wondering if we could use something like gn instead. For example

gn range(start: usize, end: usize) -> usize {
    for i in start..end {
        yield i;
    }
}

That way we’ll still know it’s a generator function, and it will have a nice symmetry with the fn keyword.

3 Likes

fn is to function as gn is to generator.

2 Likes

Please be constructive.

I do agree that gn is a bit obscure, though. People can understand fn, but more niche features should have more explicit syntax. “nice symmetry” doesn’t always mean it’s easier to understand for newcomers. async is nice, and fn* matches what some other languages do, though I’d prefer async.

(I also think that async/await should be used for something that works with futures, and fn* or generator be used for closure-like state machines like the ones stateful provides)

3 Likes

Ok, But in all seriousness, I constantly see suggestions coming from the dynamic languges’ crowd to add features that simply do not make sense in a statically typed language. There are core design tradeoffs here and Rust will never be Python or JavaScript, nor should it try to.

For the question at hand, why would we need any special syntax at all?? Again, Rust is a statically typed language and already provides the required information in the function signature, it has a return type. For the purpose of the MVP having a macro-attribute is fine and it already has one very smart design choice (kudos to @alexcrichton ) that it alters the return type, thus not repeating the same information twice but for a final design that will be integrated into the compiler and we won’t need any such macro.

Finally, I do not see any benefit from distinguishing syntactically between generators and async/await coroutines. Both implement the same concept of resumable functions and it is literally making the same distinction Pascal used to make by differentiating “Procedures” from “Functions” - await and yield both stop execution of the current function except that the latter returns a value whereas the former does not.

There have been plenty of times where I’ve felt like a generator would be a much more elegant and concise solution for a problem I’ve had in Rust. I’ve been mostly doing static languages for the last three years. It’s very dismissive to just label these as “dynamic language suggestions that don’t make sense”; generators make almost as much sense as closures do.

Yes, it’s a valid question whether or not we actually need syntax for this. A builtin smart macro would work too.

2 Likes

I'm not sure I like the idea of tagging #[async] on trait function declarations.

When I'm looking at a function signature, I don't care if it's implemented as a coroutine, or if it's implemented by imperatively chaining some futures together, or by returning some concrete object - the only thing I care about is that the return type is awaitable. One implementation may use a coroutine, another implementation may return a chained future. I don't think #[async] annotations on trait functions adds any real value.

trait AsyncRead {
    fn read_async(&mut self) -> impl Future<...>;
}

Sorry, I've evidently failed to convey my opinions on that. Here's how I feel on about borrowing-across-suspend-points:

  • for experimentation and early development: not required
  • to be "usable": required (though we might be on different definitions of usable)

Other than that, I love everything else you've said.

I don't have a strong opinion on the whether we use fn, fn* or some other syntax, but I do care very much that function signatures return something that conveys their meaning -- a generator that returns some number of values should return an Iterator or something equivalent that describes it as such, and an function that returns a deferred value should return a promise, future, or whatever.

I think that's especially important for traits (as I explain above). So instead of returning "usize" as in your example, I would like to see:

fn range(start: usize, end: usize) -> impl Iterator<Item = usize>

I'm not sure what other people think, though. How does everyone else feel?

6 Likes

Nowhere in my reply have I dismissed generators as a feature that doesn’t make sense!

What I’m critical about was the suggestion that Rust needs special syntax for function signatures in order to support generators.

Not every syntax that works in JS or Python will fit or make sense in Rust and I’d like people to think critically how their syntax suggestion will fit in the context of Rust before opening yet another RFC just because they are used to do things a certain way in some other language that made completely different core trade-offs.

At my day job I’m programming with C# which has way too many features that do not fit together at all and it fills like there was no critical thinking of that sort at all when designing the language. In particular, I saw in our code base a few places that generated warnings because we ended up somehow with an async function that doesn’t have any awaits. In addition, the MS convention is also to suffix the function name with “Async” so this information actually appears trice!

1 Like

I actually think gn is quite ingenious and I like the symmetry as well, but I agree it would be too cryptic to adopt in practice. Shortening to just two letters works for a word as ubiquitous as “function”, but “generator” is not nearly at the same level.

6 Likes

You did:

You're talking about a feature not making sense. In context, this feature would be generators/coroutines.

Anyway, this is getting off topic. If you have reasons that Rust should not adopt special syntax, feel free to provide them. Vague insinuations that folks proposing things do not understand the needs of compiled languages are presumptuous and not constructive.

6 Likes

When designing a syntax, please remember that it also needs to work for closures.

2 Likes

I have provided reasons why Rust should not adopt special syntax. I agree with @annie that the return type should provide the type information that conveys the actual meaning.

If we are to anally analyze every word I wrote (English isn’t my first, nor second language, yet I don’t feel that my words are that incomprehensible): I made a general statement about the general sentiment that people suggest “things” (better?) they are familiar with from other languages without enough regard (in my personal opinion) how those “things” integrate within the design of Rust. Then, I’ve explained how said general sentiment applies to our specific subject where I explained that the function signature has a return type (i.e Future or whatever) therefore there is no need to add to the “function signature”.

I feel really annoyed that people feel it is more crucial to the development of Rust to anally analyze any sentiment on an online text-only forum and make sure no one’s feelings were hurt by some anonymous text from across the world than to actually consider the technical merit of what was written. We are using a discussion board for a programming language after all, not a “feel good with yourself” board. In fact, I think we might actually agree here…

Anyway, i’m going to uninstall rust. I’m tired of this CoCing around. I wanted to join a technical discussion, not a political movement which tries to limit my ability to express myself.

(if people wish to discuss that interaction, please contact rust-mods@rust-lang.org, let’s not add further noise to this thread)

8 Likes

Looking over some of the recent discussion it’s got me thinking a bit, and I think I’m able to clearly articulate a good question for landing an initial implementation of generators/coroutines to the compiler. How certain to we want to be of syntax before landing in the compiler? The cricial piece here though landing in the compiler, not stabilization. I would want to have clear community consenus on all syntactical decision before stabilization, of course!

I would personally think that we don’t need to 100% nail down the syntax before landing this as an unstable feature on nightly (i.e. for the time being). Much of async/await, for example doesn’t actually end up exposing the underlying syntax for generators/coroutines in the end anyway! Now that’s not to say we should take simply anything, however, because I think certain decisions will affect the implementation in the compiler as well.

One of the major differences I noticed between @vadimcn’s RFC and @Zoxc’s implementation is how a generator/coroutine is defined. In the RFC a coroutine was a “closure with the yield keyword in it” whereas in the implementation you call a function/closure with a yield keyword to get a generator. For example, in @vadimcn’s RFC you’d write:

fn range(a, b) -> impl Generator {
    || {    
        for x in a..b {
            yield x;
        }
    }
}

whereas in @Zoxc’s implementation I believe you write:

fn range(a, b) -> impl Generator {
    for x in a..b {
        yield x;
    }
}

This, I believe, has a lot of ramifications on the implementation in the compiler itself. The way typechecking and such interact I suspect would change at a relatively deep level depending on the decision here. I believe, though, that there’s another feature in @Zoxc’s branch called gen arg to support this syntax as well, but I’m not sure I quite understand it. @Zoxc maybe you can elaborate here?

I personally feel that from a compiler implementor’s perspective that @vadimcn’s construction is the way to go. It’s got a clear definition of what a coroutine is, when code does and does not run, how captures/arguments work, etc. From a user’s perspective, however, @Zoxc’s implementation seems clearly superious as there’s basically less syntax involved.

So to me one question is: how do we resolve this? There’s sort of two questions here, though: the short term resolution and the long term resolution. I’d personally think that in the short term we should take routes like @vadimcn proposed which seem more conservative from an implementation perspective (less impact on the compiler). Once we’ve got experience with generators and coroutines, though, we can revisit decisions like this and see how much the ergonomic argument comes into play.

So @Zoxc does that sound accurate? Do you disagree with anything? Or @vadimcn am I missing something? It’s worth nothing that decision like this (and in general, most coroutine/generator decisions) don’t actually affect async/await at all! The async/await desugaring is easily translatable regardless.


In general though I think that this is an example of syntactic question that’d be good to classify. In some sense I think a lot of these can fall in the bucket of “do we need to resolve this in the short term?” I’d imagine that 99% of these questions need to be resolved by the stabilization phase of generators, but less so for the implementation phase.

@nikomatsakis or other compiler/lang folks, are there specific issues that you’d like to see resolved before landing an unstable implementation? So far a point about a test suite has been raised which I’d be more than willing to help write myself, but I’m curious if there’s other key points about generators you’d like to see resolved before landing an unstable implementation vs before stabilization.

1 Like

These questions are exactly why I wanted to make async/await available through a feature flag without also making generators available at the same time. I also doubt that the two RFCs we’ve had cover the entire breadth of the design space for generators.

In contrast, at least for the most conservative version of async/await (ie not covering returning streams and so on) the open questions are much narrower in scope.

To do that async/await would need to be compiler built-ins (instead of library macros as they are in your branch) but that’s fine with me while we work through this space. But this makes the question of which generator system to implement at first only about what is easiest to implement.

2 Likes

Just to clarify, how do you see “hygiene” working in terms of what the implementations would look like? Is there a way to put async/await into libstd without putting the futures crate itself into std?