[Post-RFC] Stackless Coroutines

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?

Hrrr you're right, there's no way to make these built-in without making Future a lang item.

2 Likes

@Zoxc great work! Finally being able to prototype on top of working generators is huge. @alexcrichton thank you for MVP and making everybody aware of this branch.

I also hacked a quick prototype https://github.com/rozaliev/async-await-rs/blob/master/src/main.rs

It feels great to write code in this style, especially when async code looks exactly like sync one just with await added.

I think that there is no immediate need for async/await support in the compiler. Generators/coroutines is enough. Putting async/await into std now may actually hurt rust ecosystem, because it will prevent people from experimenting.

I feel like adding Co/Gens will spark a new wave of network libraries and might force futures-rs to undergo some significant changes. I can’t wait to see that coming.

I have almost the same story as @annie: we use rust at my company but the current state of the networking ecosystem is preventing us from going all in. We are just afraid of ending up with 100 of thousands of lines of spaghetti code that nobody knows how to debug.

I’m going to play a bit with @alexcrichton’s MVP and then focus on building a simple library on top of mio/generators to test some edge cases. The only thing that scares me atm is that some code can be invalid due to borrow checker being too permissive.

btw there is a weird ICE if you try to call a function from a different crate that returns impl Generator if someone knows how to fix it I would appreciate some help.

One of the design decisions of both @vadimcn’s RFC and my implementation is to move async/await concerns to libraries. This avoids having future and stream concepts be a part of the language. So it doesn’t really make sense to expose async/await without also exposing generators to users.

gen arg allows you to refer to the argument for Generator::resume. Let’s look at @alexcrichton’s examples above, but print the argument passed to the generator in the loop.

@vadimcn’s RFC uses closure arguments to pass the arguments of the first resumption. All later arguments are returned as tuples by yield.

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

In my implementation, the argument to the resume function is implicit. It does not appear anywhere unless you refer to it by gen arg. It acts like a normal variable, except it is not live when the generator is suspended. yield does not return a value here. Unlike @vadimcn’s RFC, the way to access the arguments is the same at any point in the body.

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

@vadimcn’s RFC reuses closure types as generator types. This is also the main reason implementing it would result in less compiler changes. However I feel that doing this is not user friendly or very type safe. It would be easy to be confused between generators and closures, which has very separate use cases. It would also increase the chances of introducing bugs to closures, while my branch has separate code for generators. Anyway I feel like difference in implementation concerns aren’t significant here and we should focus on language concerns.

I think @vadimcn’s RFC makes common use cases like iterators and async/await unwieldy and would be against the new ergonomics initiative. People will use compiler plugins to make it ergonomic, and we’d need such a plugin for each use case of generators (futures-rs, iterators, microkernel servers, etc.). I do not think designing language features which users won’t directly use is a good idea.

Having an utility closure makes difference between these two proposals more clear.

@vadimcn’s RFC:

fn foo() -> impl FnMut() -> CoResult<(), Value> {
    ||
        let bar = |arg| {    
            || {
                await!(task(arg + 1));
                await!(task(arg + 2));
            }
        };

        await!(bar(1));
        await!(bar(2));
    }
}

My implementation:

fn foo() -> impl Generator<Return=Value, Yield=()> {
    let bar = |arg| {    
        await!(task(arg + 1));
        await!(task(arg + 2));
    };

    await!(bar(1));
    await!(bar(2));
}

I want to block landing my branch in rustc on whether we go with an approach like @vadimcn’s RFC or what I implemeted. This is mostly due to the fact that changing the implementation is easier out of tree. I don’t think we should block on syntax, as changes to syntax is trivial.

For allowing borrows across suspend points, immovable types provides an solution. Now this probably requires marking generators as immovable or movable upfront, so we require syntax for that. The reason for this is that I do not know how to find out of a generator borrows across suspend points during type checking. Non-lexical lifetimes will probably make this impossible until after MIR generation.

Now if you feel like bikeshedding syntax, what is desired is syntax for marking bodies of function and closures as generators. We also require to mark generators as either movable or immovable. Note that this only applies to the bodies of function and closure and does not affect their signature. The syntax should also be consistient and not differ for function and closures. So using say gn instead of fn only works for functions. It is also a bad idea for a variety of other reasons. I don’t think using trait bounds on impl Trait is a good idea for this. impl Trait + ?Move should be a guarantee to the caller that the type never implements Move, it should still be legal to return any movable type (including movable generators) it would just be treated as an immovable one. These things should be up to the impl Trait language feature, which while useful is orthogonal. Also such bounds aren’t yet possible for closures. They would be very verbose for closures and will hide the underlying generator type which isn’t necessary.

There seem to be consensus for using yield for suspending generators, so we should use that.

My implementation allows you to refer to the argument to generators with gen arg. The syntax for this is up for bikeshedding. You may also argue on the names of the Generator trait, State struct and everything contained inside them.

Syntax for @vadimcn’s RFC would be a bit different as there is no gen arg and it only requires marking closures as movable or immovable generators.

@rozaliev Your code is almost how my branch is intended to be used. Instead of returning impl Generator, you should define a future trait, implement that for all suitable generators and just return impl Future instead. Instead of having a Handle type you should use for<'c> Generator<&'c mut Core>> instead. You can then have unique access to the core with gen arg. This doesn’t actually work in my branch yet. You can achieve a similar user interface by storing the Core in TLS in the mean time. This just avoid passing around handles in your code.

Known bugs in my implementation:

  • Generators never implement any OIBITs
  • Generators doesn’t contain the types for variables which are live across suspend points from a type system perspective. This can result in unsoundness when generators actually contain types with lifetimes.
  • Borrowing local variables across suspend point result in unsoundness if the generator’s memory location changes after it has resumed once.

If you find any further bugs in my implementation (https://github.com/Zoxc/rust/tree/gen), feel free to file an issue here: https://github.com/Zoxc/rust/issues

Wait a second...
Firstly, I think the function header will be this in both cases:

fn foo() -> impl Future<Item=Value, Error=SomeError>

(assuming you are talking about async I/O, which I think you are, since you use await!'s)

Secondly, what is this code supposed to do? Who provides arg to the closure? await! needs a Future, not a closure returning a Future, so in your implementation, don't you need to invoke bar?

That is true. I was just showing the underlying traits for comparison purposes.

I messed up there. I was supposed to call bar. I fixed the examples.

I concede, in this instance your approach is more concise (though mine can get very close with #[async]).

On the other hand, here are some arguments in favor of my proposal:

Inline iterators My:

function_taking_an_iterator(|| {
    ...
    yield x;
})

Yours:

function_taking_an_iterator((|| {
    ...
    yield x;
})()) // Need to invoke closure to get the iterator.

^^^ this looks a bit weird…

Capture of upvars By default, normal closures capture upvars by reference. Your generator closures look very similar, but always capture by move (if I understood it correctly).

Closure arguments

  • Are you proposing that coroutines can take at most one argument? Ok, it can be a tuple, but this lowers ergonomics for the caller.
  • Since your gen arg is implicit, you cannot specify its type in case type inference is not sufficient.
  • Also, an “ambient variable” like that is not very “Rustic”. IMO.

Symmetry with how “normal” iterators are created

Normal:

    fn into_iter(self) -> impl Iterator<Item=T> {
        // Create and return an object implementing Iterator.
        MyIter { 
          field1: ...,
          field2: ...,
          ...
        }
    }

My proposal:

    fn into_iter(self) -> impl Iterator<Item=T> {
        // Closures are objects too...
        || {
            ...
            yield x;
        }    
    }

Personally, I like the symmetry between these two cases.

4 Likes

I feel I may have been misunderstood. I wanted to expose async/await without generators temporarily until we had resolved more of the design questions around generators. There's no reason we can't have the more abstract form temporarily accessible while its implementing form is not. We do this all the time in terms of stable/unstable (closures and the Fn traits are a long running example.

Syntactically built-ins look like attributes and macros and there's no reason async/await couldn't be moved into a lib once the generators API was stable.

However I don't know how we could do this without moving Future into libcore so the question is moot.

I'm in favor of accepting anything that is easy to delete & messaging to users that the generator API will churn (this would be very extreme, but we could try Zoxc's proposal for 3 months, then vadimcn's for 3 months, and collect feedback).