-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: introduce the flavor syntactic design pattern #3710
Conversation
So, I understand why you're referring to these as colours, since "coloured functions" is what the larger programming language community uses as a term, but I think that the naming of effects should be used instead, especially since that's at least what the current Rust WGs have been settling on. Can you think of an example of a "colour" by this metric that wouldn't be an effect? And even then, I think it would be say that the colour should have an effect on everything it labels, so, that meaning would still apply. The biggest benefit to this is that "effects" also clearly evoke the purpose of the colouring: the async effect means that everything is designed for an async context, and the meaning is that return values are wrapped in futures. Similarly, the const effect means that everything is designed to be evaluable in const context. Also just as a small picky addition: that bicycle emoji is impossibly small without zooming in and while I appreciate the joke, it is unnecessarily opaque and adds in extra effort where the word "bike" or "bicycle" or "bikeshed" would probably be a lot clearer. |
I avoided the term "effect" for a few reasons. One of them is that I think that it is overall kind of jargon. What's more, my observation is that it is divisive jargon, as people bring pre-conceived notions of what it ought to mean, and not everything that I consider a "color" fits into those notions. My take on an effect is that it is some kind of "operation" that occurs during execution, such as a write to a specific memory region, a panic, a memory allocation, etc. It's reasonable to model this kind of effect as a "function" you can call when that event occurs (perhaps with some arguments). From what I can tell, this definition lines up with Koka (which is a very cool language). However, Koka is also (I believe) based on Continuation Passing Style, which means that simple function calls get a lot more power. This allows them to model e.g. generators or exceptions as effects. To my mind, this is kind of cheating, or at least misleading. In particular, we can't "just" port over Koka's abstractions to Rust because we also have to account for rewrites. In any case, I'm open to a terminology discussion, though personally I'd be inclined not to rename colors to effects, but perhaps to rename filter colors to effect colors or effect-carrying colors. EDIT: (added as a FAQ) |
Koka's use of CPS is not at all fundamental to its semantics of effects- it is an implementation detail. The semantically-interesting thing, shared in common with other languages' "(algebraic) effects," even when they are not implemented via CPS, is that those effect operations can be handled by suspending and resuming the program. Rust also does the same thing via a different implementation strategy, a related lowering to state machines. It would be perfectly legitimate to call things like |
one more effect/color came up: |
I mean, that is fair that it does give you some sort of preconceived notion, although I think that we've done a pretty good job reconfiguring some of these terms in Rust. To me, both "colour" and "effect" are equally jargon, but "effect" gives me at least an idea of what the jargon means, whereas "colour" gives me nothing at all. Like, if you're going to go with jargon at all, then you should at least try to use jargon that gives people a basic idea that they can latch onto after they know what the jargon means— this is why we use terms like "result" instead of "flargleflorpus" in Rust. I would also additionally like to challenge that "effect" strongly confers the notion that something occurs at runtime, although we could make everyone upset and use "affect" instead. To me, this merely affects the things that are labelled with it, which… actually, that's two arguments for "affect" over "effect"… |
I can definitely say that "color" is absolutely a jargon word when used for functions. Separately, the bike emoji is cute to put in the RFC, but it doesn't render very well all the time. On my particular device, it's a small gray bike in a gray background tele-type text span, and it ends up almost disappearing from view unless you know you're looking for it. Might want to use an actual word like "bikeshed" instead. |
maybe use a different bicycle character: 🚴 -- it likely renders with more color. |
This is what I was referring to, yes. Basically, doing those transformations in an omnipresent way. This has more to do with what it would mean for us to support "user-defined" colors than anything else (which obviously is way out of scope, especially for this RFC.) |
This is... a lot to take in. This RFC proposes a lot of magic but doesn't provide a concrete example (eg. with async). It describes being able to apply "async" to get a variation of a trait, but I'm not convinced that's... good? For example Also, these colours are described as "rewriting" the item in question, but a big problem with Rust's async is that we can't even express many of the types (like async closures) yet, so what exactly are these types going to be rewritten into? |
Will this RFC or the future consider commitment about multi-colored items
|
This is totally avoidable and probably should be avoided. This has already been the subject of a lot of discussion: for example, see boats' https://without.boats/blog/poll-next/ and my https://www.abubalay.com/blog/2024/01/14/rust-effect-lowering The general idea is that effect-carrying colors do not really make sense to compose via nesting. This is less obvious with So if this RFC were to cover this at all, I would expect it to lean toward supporting multi-color blocks, but with the ordering of the colors being semantically irrelevant. (Indeed this is essentially what makes "algebraic effects" "algebraic.") |
Putting the colors/effects in the type system prevents this from being an omnipresent thing. Koka uses a selective CPS transform that only touches effectful functions; Rust equivalently only applies the state machine transformation to My point is really that "effect" need not imply anything so deep about the language that it does not already apply to Rust, and specifically that Koka's use of CPS is not really relevant to how Rust might use the term. |
This is a small nitpick, but since this is kind of a theory-heavy RFC, it felt worth pointing out that:
…is not true. Things can be algebraic and also order-dependent, and so it's not immediately obvious why we should adopt an order-independent approach. Sure, the example you gave was reasonable, but it's unclear whether all future versions of effects/colours will fit those descriptions. |
OK, I see. I stand corrected. Cool! I will weaken my FAQ answer later today. =) I still prefer 'color' as the term over 'effect', but I guess I would say...I am persuadable. I agree that color is itself jargon but it strikes me as far more approachable and evocative jargon than effect. I think there's a reason the blog post was called "what color is your function" and not "what effects does your function have". |
I think it is a lot less to take in than it may appear. I view this RFC as documenting existing patterns and "rounding them out" more than it is creating new ones. The only brand new thing in this RFC is committing to some syntax like
The RFC is not proposing a mechanical translation. Does that make sense, @Diggsey ? (I plan to create a FAQ from this conversation once we reach a fix point.) |
I feel like I think that “ |
OK, I want to engage more deeply on the naming question. It's important and I don't think color is necessarily optimal. Here are some thoughts. When it comes to names, I think there are several qualities that the ideal name ought to have:
There have been three names proposed, two on this thread, and one by @Nadrieril on Zulip: effect, color, and flavor. Looking at these, I analyze them as follows. Effect I think is reasonably memorable and specific, and it is the term I started with. I adopted color because (a) I have not found effect to be familiar nor approachable to people in conversation but also (b) it is not grammatically flexible. For example, the RFC frequently talks about Color is better but I see some downsides. First, it is such a common word in conversation that I can imagine it not being obvious it's a "term of art" here. It would be very hard to ever make it a keyword, if we found a reason to do so. Second, although I frequently see color used in the sense I mean it in informal conversation (typically referencing the "What color is your function" blog post), I wouldn't say it's familiar exactly. Third, color is relatively grammatically flexible, but I still found it not ideal. I wanted sometimes to talk about a specific "version" of a trait, and saying "the async color of the Trait" didn't sound right. There was also the problem that, as y'all are hopefully aware, the term "colored" has been associated with some awful parts of human history, and I found myself being careful about how I used the word color when writing the RFC to try and avoid evoking those connotations. All in all, not ideal for a word that may wind up being used a lot. So this morning I've been pondering @Nadrieril's suggestion of flavor. It has all the advantages of color (familiar, memorable, approachable) but it seems to me to be actually more specific than color (I can imagine it becoming a keyword someday, for example, without quite the level of pain that color would have). It is also more grammatically flexible: we can talk about the flavor of a trait and that sounds very good, and it has no negative connotations. So I'm strongly considering switching the RFC to adopt flavor. I'd be curious to hear a similar argument made in favor of effect. It's worth listing out the ways we'd like to use the term. I'll give examples alternating through the proposals and (typically) quoting from the RFC:
It's worth pointing out that whatever word we choose, it's a 2-way door. This is a name for a design pattern that we use to maintain internal consistency for the language. If we find that the name doesn't work, we can change it, and the only thing that becomes outdated is our internal design docs and blog posts. That said, I do think the word will wind up "leaking out" into how people talk about Rust, so it's worth investing some thought into it and trying to get it right the first time. |
I'm thinking more about this...
I agree with you here but I still find there is something that's bugging me. I think it's this. I like being able to think of "effect" as a shorthand for "side effect". But if one of the elements of a "side effect" is that it translates your function into a coroutine -- and potentially introduces a new way for the function to take input from the outside, as in the most general case of So I agree that I was overrotating on the continuation passing style implementation detail, but I think I still see value in limiting the term "effect" to "side effects". That said, if we only think of generators emitting values (and not getting values back in as input), I can see it as a generalization of side effect that works. It just seems to go beyond my intuitions, but I could get used to it. It does explain why (UPDATE: I guess you can think of |
Responding to my response to @Diggsey, @GoldsteinE wrote...
Ah, I see-- yes, I get that. I agree that the I think the point of the RFC is not that trait Read {
fn read();
}
trait async Read {
fn poll_read(self: Pin<&mut Self>);
fn read() -> async -> () {
/* something defined in terms of `poll_read` */
}
} ...or it can be some way to automatically create What the RFC is saying is that we aim for In any case, I agree with you that this is a new thing in the RFC (and I tried to highlight it as such). |
It is not obvious that having Unless, of course, I think even the ability to have |
I think it’s pretty uncontroversial for |
Ok, that does explain it better. It does raise some concerns about namespacing though. If |
@Diggsey You would import
If you write In short, you can think of a trait like (Bear in mind that all of this is going beyond what's specified in the RFC, which specifically avoids saying what |
I guess it’s just weird that it says that we should have it mean something, without exploring whether there is a meaning that makes sense. |
To maybe help frame this: Koka differentiates between effect types and effect handlers. Effect handlers are sometimes also referred to as typed continuations. Effect handlers in Koka are expressed in the type system as effect types. But not all effect types are also effect handlers. An example of an effect type in Koka which is not an effect handler is divergence ( Divergent functions in Koka are functions which do not statically guarantee they will terminate. This cannot be modeled in terms of coroutines, despite having runtime implications. Instead it directly represents a language-level capability. Daan Leijen (Koka's lead) explained this as follows:
This is why I tend to think of effect types more like language-level capabilities or permissions. With typed continuations (effect handlers) representing a special subset of those which map to e.g. |
@GoldsteinE I appreciate you raising the point and helping me to understand it. I'm going to be writing up a summary for an FAQ and bringing it to the broader lang team as part of our review from this thread. I'll drop a note here with a link so you can double check I've accurately reflected your opinion. (Same for you, @clarfonthey) |
However, given current rustfmt convention, that would get rendered as this: K9 {
K7 {
K8 {
$expr
}
}
} ...which I don't find reasonable. I don't think it's a good idea to encourage either more |
For async Read/Write traits, I consider the current state-of-the-art to be this proposal by @nrc. It proposes generic The This RFC doesn't need to answer all of those questions, but I think it's important to consider that some traits will have sync and async versions while some will only have one or the other. For some, it may not be clear at the time of stabilizing whether we will want both. This only complicates naming under this RFC if it's an async-first trait. I feel pretty confident that we can come up with a way of resolving such uncertainties, both because I think it should be pretty clear by the time of stabilizing whether we will want both, and because for any traits where it's not completely clear, we can commit to stabilizing an explicit |
I mean, given how we have a predefined ordering for the keywords, it would make senses that, for example: const {
unsafe {
expr()
}
} could be shortened to: const unsafe {
expr()
} if needed. |
So I disagree with pretty much all the reasons for not going with AsyncFn, and wanted to go over why, for each of them.
I think that story is only simpler at that high a level of abstraction. If you know what the right places are, you know why they need to change. If you know you want a different version of a trait, changing the name is what you do every other time that's what you need, so that seems simpler to me. If you don't know you want a different version of a trait, just adding a keyword means you still may not understand what the change did, where changing the name is extremely clear. Also, even if that's the story we want, it's not going to be the case for any traits outside the standard library. Consistency across the ecosystem seems important; if we eventually give the rest of the ecosystem the tools to change and encourage them to do so, we can change the standard library then as well.
Absolutely right. AsyncTrait is the obvious solution for async traits, and doesn't work for const. I don't think that means we need some other consistent method, I think it means those are fundamentally different things and treating them as the same is a false equivalency that would make the language harder to learn.
Keywords would also become unwieldy in this case. Only differences I can see is that keywords might not require a specific order, and that long names benefit from auto-completion and the ability to import as a shorter name.
You just have AsyncFn be an alias for the generic version with the async parameter true. It's not at all clear to me that that wouldn't be desirable in general - being generic over async is extra complexity that is pretty often unwanted. My biggest concern with the idea of flavor generics is that they could make learning the language, and the standard library in particular, more complicated for everyone. I don't think committing to the extra complexity in the standard library even before we've committed to flavor generics is the way to go. For the last point, and just in general, I consider keywords to be quite complicated syntax; since they can be used for pretty much anything, you have to specifically learn what they mean in each case. If there's a question of using a keyword or some other method, I lean towards the other method. |
The lang team will be discussing this RFC in a design meeting -- I think today but I might be wrong -- and so I prepared a summary of the discussion here: https://hackmd.io/w2-etPU7RouzxJ6GJ1fbSg Feedback welcome. @PeterHatch I hadn't seen your comment yet so I'm going to read it over and make sure it's reflected in there. Some sections to note... I also took another stab at the RFC summary and framing that I'd be curious to get people's read on (is it clearer?):
Please feel free to leave hackmd comments in the doc. |
So, to add context to my earlier comments, somewhat discussed under "Non-mechanical mechanisms" in the HackMD - I think the issue was that I wasn't clear about the steps that got me to my opinion, and assumed they were shared. To be specific, I think:
|
On the section of the meeting notes about It doesn't make sense to apply another flavor to a flavor-carrying trait, because it mixes registers or levels of abstraction. This is the exact same thing that has been thoroughly discussed in terms of streams. It is also something that RFC #3628 might address- you can freely add/remove flavors to something like (Edit, reading further: To be clear, this is not unique to |
Sorry for being late to this discussion. I just saw these traits, and I think they're way inferior to the current poll-based designs used by Tokio or futures. The problem is that you're making The way this is worked around in std is by implementing Now, you might argue that the Even if you add " Edit: This is probably off-topic. Please reply to this comment here instead. |
I have some concerns about introducing 🏠K<$ty>. To understand a flavor is to understand its output type. I can't really explain a try block without telling you about Result, or gen without Iterator, or async without Future. This is demonstrated in the "teaching" section of the RFC where you define what a future is in the first few sentences of teaching async functions. So I don't think that hiding away that type with another syntax helps with learnability of the feature, because learning that specific type is critically important to understanding the flavor. If anything, naming that type in the code is helpful. For devs of any experience level, how are we going to pronounce async<T>? I'd have to say "a future of T". The syntax would just necessitate some mental translation to read it. Is there even one other possible flavor where that syntax might work? All the other parts of the language that can be made consistent makes sense to me, but the types really are special and should not be over-abstracted IMO. |
One thing that may not be immediately apparent is that the stabilization of RFC 3668 async closures had become blocked on this RFC. For that feature, we have to choose, as the bounds syntax, between We had agreed, in later lang discussion, that we might have consensus on On the plus side, there are many valuable bits in this RFC, and I hope that we are able to make something of those later. I'm glad that @nikomatsakis wrote this up, and working through this has I think sharpened all of our thinking. The resulting discussion on this thread and in Zulip has been particularly illuminating, and many people have made a number of compelling points. But overall, I've come to believe that this RFC is trying to do too much and is foreshadowing too much when we have too many question marks. It's proposing a big model, and I'm just not sure that we've really nailed it. As one concrete matter, while I was supportive of As has been discussed on this thread, where things get particularly difficult is when the trait that would be affected represents the state machine for some other "K". I remain strongly skeptical that we would ever want to have Looking beyond that, I sense that it's harder to apply this At the same time, I believe that we will eventually have a first class "K-style" notion in the language. I'm just not confident that notion necessarily implies In that light, I particularly want to decouple async closures from this RFC and allow those to move forward. Personally, I've warmed to shipping async closures with In support of this, @nikomatsakis has pointed out in discussion that, since we wouldn't be soon shipping any kind of generalized form that would allow the ecosystem to write I agree with and am persuaded by that also. I believe it's likely at this point that we will decouple async closures from this RFC and proceed to propose they be stabilized using the As mentioned, there are many bits of this RFC that I do find valuable, and I hope that we do come back separately to these and are able to benefit from the careful thinking that went into this document and into the discussions that have been prompted by it. |
From the meeting notes:
Note that needing to import a trait manually helps avoid new ambiguities being added to code due to changes in other crates. Opting out of that seems like it could cause problems.
If I'm understanding correctly, under this proposal flavored traits are supposed to have the same members as the base trait, meaning |
So, building on what @traviscross said, I'm inclined to close the RFC. I definitely feel disappointed, as writing the RFC helped clarify many things and I liked the idea of reducing the uncertainty a bit. But I also think it will be just fine to stabilize Like TC, I ultimately do believe we should have some form of However, I don't think we need to stabilize |
I don't think this (discussion about more general "K-flavoring") really changes anything about the idea that the More specifically: regardless of whether we start with |
I think this is the right choice - it will be much easier to discuss this idea once we have a more concrete idea of how the syntax would be used in practice. |
…aheemdev Add `AsyncFn*` to the prelude in all editions The general vibe is that we will most likely stabilize the `feature(async_closure)` *without* the `async Fn()` trait bound modifier. Without `async Fn()` bound syntax, this necessitates users to spell the bound like `AsyncFn()`. Since `core::ops::AsyncFn` is not in the prelude, users will need to import these any time they actually want to use the trait. This seems annoying, so let's add these traits to the prelude unstably. We're trying to work on the general vision of `async` trait bound modifier in general in: rust-lang/rfcs#3710, however that RFC still needs more time for consensus to converge, and we've decided that the value that users get from calling the bound `async Fn()` is *not really* worth blocking landing async closures in general.
Add `AsyncFn*` to the prelude in all editions The general vibe is that we will most likely stabilize the `feature(async_closure)` *without* the `async Fn()` trait bound modifier. Without `async Fn()` bound syntax, this necessitates users to spell the bound like `AsyncFn()`. Since `core::ops::AsyncFn` is not in the prelude, users will need to import these any time they actually want to use the trait. This seems annoying, so let's add these traits to the prelude unstably. We're trying to work on the general vision of `async` trait bound modifier in general in: rust-lang/rfcs#3710, however that RFC still needs more time for consensus to converge, and we've decided that the value that users get from calling the bound `async Fn()` is *not really* worth blocking landing async closures in general.
Add `AsyncFn*` to the prelude in all editions The general vibe is that we will most likely stabilize the `feature(async_closure)` *without* the `async Fn()` trait bound modifier. Without `async Fn()` bound syntax, this necessitates users to spell the bound like `AsyncFn()`. Since `core::ops::AsyncFn` is not in the prelude, users will need to import these any time they actually want to use the trait. This seems annoying, so let's add these traits to the prelude unstably. We're trying to work on the general vision of `async` trait bound modifier in general in: rust-lang/rfcs#3710, however that RFC still needs more time for consensus to converge, and we've decided that the value that users get from calling the bound `async Fn()` is *not really* worth blocking landing async closures in general.
…, r=lcnr Gate async fn trait bound modifier on `async_trait_bounds` This PR moves `async Fn()` trait bounds into a new feature gate: `feature(async_trait_bounds)`. The general vibe is that we will most likely stabilize the `feature(async_closure)` *without* the `async Fn()` trait bound modifier, so we need to gate that separately. We're trying to work on the general vision of `async` trait bound modifier general in: rust-lang/rfcs#3710, however that RFC still needs more time for consensus to converge, and we've decided that the value that users get from calling the bound `async Fn()` is *not really* worth blocking landing async closures in general.
Rollup merge of rust-lang#132612 - compiler-errors:async-trait-bounds, r=lcnr Gate async fn trait bound modifier on `async_trait_bounds` This PR moves `async Fn()` trait bounds into a new feature gate: `feature(async_trait_bounds)`. The general vibe is that we will most likely stabilize the `feature(async_closure)` *without* the `async Fn()` trait bound modifier, so we need to gate that separately. We're trying to work on the general vision of `async` trait bound modifier general in: rust-lang/rfcs#3710, however that RFC still needs more time for consensus to converge, and we've decided that the value that users get from calling the bound `async Fn()` is *not really* worth blocking landing async closures in general.
Gate async fn trait bound modifier on `async_trait_bounds` This PR moves `async Fn()` trait bounds into a new feature gate: `feature(async_trait_bounds)`. The general vibe is that we will most likely stabilize the `feature(async_closure)` *without* the `async Fn()` trait bound modifier, so we need to gate that separately. We're trying to work on the general vision of `async` trait bound modifier general in: rust-lang/rfcs#3710, however that RFC still needs more time for consensus to converge, and we've decided that the value that users get from calling the bound `async Fn()` is *not really* worth blocking landing async closures in general.
…i-obk Stabilize async closures (RFC 3668) # Async Closures Stabilization Report This report proposes the stabilization of `#![feature(async_closure)]` ([RFC 3668](https://rust-lang.github.io/rfcs/3668-async-closures.html)). This is a long-awaited feature that increases the expressiveness of the Rust language and fills a pressing gap in the async ecosystem. ## Stabilization summary * You can write async closures like `async || {}` which return futures that can borrow from their captures and can be higher-ranked in their argument lifetimes. * You can express trait bounds for these async closures using the `AsyncFn` family of traits, analogous to the `Fn` family. ```rust async fn takes_an_async_fn(f: impl AsyncFn(&str)) { futures::join(f("hello"), f("world")).await; } takes_an_async_fn(async |s| { other_fn(s).await }).await; ``` ## Motivation Without this feature, users hit two major obstacles when writing async code that uses closures and `Fn` trait bounds: - The inability to express higher-ranked async function signatures. - That closures cannot return futures that borrow from the closure captures. That is, for the first, we cannot write: ```rust // We cannot express higher-ranked async function signatures. async fn f<Fut>(_: impl for<'a> Fn(&'a u8) -> Fut) where Fut: Future<Output = ()>, { todo!() } async fn main() { async fn g(_: &u8) { todo!() } f(g).await; //~^ ERROR mismatched types //~| ERROR one type is more general than the other } ``` And for the second, we cannot write: ```rust // Closures cannot return futures that borrow closure captures. async fn f<Fut: Future<Output = ()>>(_: impl FnMut() -> Fut) { todo!() } async fn main() { let mut xs = vec![]; f(|| async { async fn g() -> u8 { todo!() } xs.push(g().await); }); //~^ ERROR captured variable cannot escape `FnMut` closure body } ``` Async closures provide a first-class solution to these problems. For further background, please refer to the [motivation section](https://rust-lang.github.io/rfcs/3668-async-closures.html#motivation) of the RFC. ## Major design decisions since RFC The RFC had left open the question of whether we would spell the bounds syntax for async closures... ```rust // ...as this... fn f() -> impl AsyncFn() -> u8 { todo!() } // ...or as this: fn f() -> impl async Fn() -> u8 { todo!() } ``` We've decided to spell this as `AsyncFn{,Mut,Once}`. The `Fn` family of traits is special in many ways. We had originally argued that, due to this specialness, that perhaps the `async Fn` syntax could be adopted without having to decide whether a general `async Trait` mechanism would ever be adopted. However, concerns have been raised that we may not want to use `async Fn` syntax unless we would pursue more general trait modifiers. Since there remain substantial open questions on those -- and we don't want to rush any design work there -- it makes sense to ship this needed feature using the `AsyncFn`-style bounds syntax. Since we would, in no case, be shipping a generalized trait modifier system anytime soon, we'll be continuing to see `AsyncFoo` traits appear across the ecosystem regardless. If we were to ever later ship some general mechanism, we could at that time manage the migration from `AsyncFn` to `async Fn`, just as we'd be enabling and managing the migration of many other traits. Note that, as specified in RFC 3668, the details of the `AsyncFn*` traits are not exposed and they can only be named via the "parentheses sugar". That is, we can write `T: AsyncFn() -> u8` but not `T: AsyncFn<Output = u8>`. Unlike the `Fn` traits, we cannot project to the `Output` associated type of the `AsyncFn` traits. That is, while we can write... ```rust fn f<F: Fn() -> u8>(_: F::Output) {} ``` ...we cannot write: ```rust fn f<F: AsyncFn() -> u8>(_: F::Output) {} //~^ ERROR ``` The choice of `AsyncFn{,Mut,Once}` bounds syntax obviates, for our purposes here, another question decided after that RFC, which was how to order bound modifiers such as `for<'a> async Fn()`. Other than answering the open question in the RFC on syntax, nothing has changed about the design of this feature between RFC 3668 and this stabilization. ## What is stabilized For those interested in the technical details, please see [the dev guide section](https://rustc-dev-guide.rust-lang.org/coroutine-closures.html) I authored. #### Async closures Other than in how they solve the problems described above, async closures act similarly to closures that return async blocks, and can have parts of their signatures specified: ```rust // They can have arguments annotated with types: let _ = async |_: u8| { todo!() }; // They can have their return types annotated: let _ = async || -> u8 { todo!() }; // They can be higher-ranked: let _ = async |_: &str| { todo!() }; // They can capture values by move: let x = String::from("hello, world"); let _ = async move || do_something(&x).await }; ``` When called, they return an anonymous future type corresponding to the (not-yet-executed) body of the closure. These can be awaited like any other future. What distinguishes async closures is that, unlike closures that return async blocks, the futures returned from the async closure can capture state from the async closure. For example: ```rust let vec: Vec<String> = vec![]; let closure = async || { vec.push(ready(String::from("")).await); }; ``` The async closure captures `vec` with some `&'closure mut Vec<String>` which lives until the closure is dropped. Every call to `closure()` returns a future which reborrows that mutable reference `&'call mut Vec<String>` which lives until the future is dropped (e.g. it is `await`ed). As another example: ```rust let string: String = "Hello, world".into(); let closure = async move || { ready(&string).await; }; ``` The closure is marked with `move`, which means it takes ownership of the string by *value*. The future that is returned by calling `closure()` returns a future which borrows a reference `&'call String` which lives until the future is dropped (e.g. it is `await`ed). #### Async fn trait family To support the lending capability of async closures, and to provide a first-class way to express higher-ranked async closures, we introduce the `AsyncFn*` family of traits. See the [corresponding section](https://rust-lang.github.io/rfcs/3668-async-closures.html#asyncfn) of the RFC. We stabilize naming `AsyncFn*` via the "parenthesized sugar" syntax that normal `Fn*` traits can be named. The `AsyncFn*` trait can be used anywhere a `Fn*` trait bound is allowed, such as: ```rust /// In return-position impl trait: fn closure() -> impl AsyncFn() { async || {} } /// In trait bounds: trait Foo<F>: Sized where F: AsyncFn() { fn new(f: F) -> Self; } /// in GATs: trait Gat { type AsyncHasher<T>: AsyncFn(T) -> i32; } ``` Other than using them in trait bounds, the definitions of these traits are not directly observable, but certain aspects of their behavior can be indirectly observed such as the fact that: * `AsyncFn::async_call` and `AsyncFnMut::async_call_mut` return a future which is *lending*, and therefore borrows the `&self` lifetime of the callee. ```rust fn by_ref_call(c: impl AsyncFn()) { let fut = c(); drop(c); // ^ Cannot drop `c` since it is borrowed by `fut`. } ``` * `AsyncFnOnce::async_call_once` returns a future that takes ownership of the callee. ```rust fn by_ref_call(c: impl AsyncFnOnce()) { let fut = c(); let _ = c(); // ^ Cannot call `c` since calling it takes ownership the callee. } ``` * All currently-stable callable types (i.e., closures, function items, function pointers, and `dyn Fn*` trait objects) automatically implement `AsyncFn*() -> T` if they implement `Fn*() -> Fut` for some output type `Fut`, and `Fut` implements `Future<Output = T>`. * This is to make sure that `AsyncFn*()` trait bounds have maximum compatibility with existing callable types which return futures, such as async function items and closures which return boxed futures. * For now, this only works currently for *concrete* callable types -- for example, a argument-position impl trait like `impl Fn() -> impl Future<Output = ()>` does not implement `AsyncFn()`, due to the fact that a `AsyncFn`-if-`Fn` blanket impl does not exist in reality. This may be relaxed in the future. Users can work around this by wrapping their type in an async closure and calling it. I expect this to not matter much in practice, as users are encouraged to write `AsyncFn` bounds directly. ```rust fn is_async_fn(_: impl AsyncFn(&str)) {} async fn async_fn_item(s: &str) { todo!() } is_async_fn(s); // ^^^ This works. fn generic(f: impl Fn() -> impl Future<Output = ()>) { is_async_fn(f); // ^^^ This does not work (yet). } ``` #### The by-move future When async closures are called with `AsyncFn`/`AsyncFnMut`, they return a coroutine that borrows from the closure. However, when they are called via `AsyncFnOnce`, we consume that closure, and cannot return a coroutine that borrows from data that is now dropped. To work around around this limitation, we synthesize a separate future type for calling the async closure via `AsyncFnOnce`. This future executes identically to the by-ref future returned from calling the async closure, except for the fact that it has a different set of captures, since we must *move* the captures from the parent async into the child future. #### Interactions between async closures and the `Fn*` family of traits Async closures always implement `FnOnce`, since they always can be called once. They may also implement `Fn` or `FnMut` if their body is compatible with the calling mode (i.e. if they do not mutate their captures, or they do not capture their captures, respectively) and if the future returned by the async closure is not *lending*. ```rust let id = String::new(); let mapped: Vec</* impl Future */> = [/* elements */] .into_iter() // `Iterator::map` takes an `impl FnMut` .map(async |element| { do_something(&id, element).await; }) .collect(); ``` See [the dev guide](https://rustc-dev-guide.rust-lang.org/coroutine-closures.html#follow-up-when-do-async-closures-implement-the-regular-fn-traits) for a detailed explanation for the situations where this may not be possible due to the lending nature of async closures. #### Other notable features of async closures shared with synchronous closures * Async closures are `Copy` and/or `Clone` if their captures are `Copy`/`Clone`. * Async closures do closure signature inference: If an async closure is passed to a function with a `AsyncFn` or `Fn` trait bound, we can eagerly infer the argument types of the closure. More details are provided in [the dev guide](https://rustc-dev-guide.rust-lang.org/coroutine-closures.html#closure-signature-inference). #### Lints This PR also stabilizes the `CLOSURE_RETURNING_ASYNC_BLOCK` lint as an `allow` lint. This lints on "old-style" async closures: ```rust #![warn(closure_returning_async_block)] let c = |x: &str| async {}; ``` We should encourage users to use `async || {}` where possible. This lint remains `allow` and may be refined in the future because it has a few false positives (namely, see: "Where do we expect rewriting `|| async {}` into `async || {}` to fail?") An alternative that could be made at the time of stabilization is to put this lint behind another gate, so we can decide to stabilize it later. ## What isn't stabilized (aka, potential future work) #### `async Fn*()` bound syntax We decided to stabilize async closures without the `async Fn*()` bound modifier syntax. The general direction of this syntax and how it fits is still being considered by T-lang (e.g. in [RFC 3710](rust-lang/rfcs#3710)). #### Naming the futures returned by async closures This stabilization PR does not provide a way of naming the futures returned by calling `AsyncFn*`. Exposing a stable way to refer to these futures is important for building async-closure-aware combinators, and will be an important future step. #### Return type notation-style bounds for async closures The RFC described an RTN-like syntax for putting bounds on the future returned by an async closure: ```rust async fn foo(x: F) -> Result<()> where F: AsyncFn(&str) -> Result<()>, // The future from calling `F` is `Send` and `'static`. F(..): Send + 'static, {} ``` This stabilization PR does not stabilize that syntax yet, which remains unimplemented (though will be soon). #### `dyn AsyncFn*()` `AsyncFn*` are not dyn-compatible yet. This will likely be implemented in the future along with the dyn-compatibility of async fn in trait, since the same issue (dealing with the future returned by a call) applies there. ## Tests Tests exist for this feature in [`tests/ui/async-await/async-closures`](https://github.com/rust-lang/rust/tree/5b542866400ad4a294f468cfa7e059d95c27a079/tests/ui/async-await/async-closures). <details> <summary>A selected set of tests:</summary> * Lending behavior of async closures * `tests/ui/async-await/async-closures/mutate.rs` * `tests/ui/async-await/async-closures/captures.rs` * `tests/ui/async-await/async-closures/precise-captures.rs` * `tests/ui/async-await/async-closures/no-borrow-from-env.rs` * Async closures may be higher-ranked * `tests/ui/async-await/async-closures/higher-ranked.rs` * `tests/ui/async-await/async-closures/higher-ranked-return.rs` * Async closures may implement `Fn*` traits * `tests/ui/async-await/async-closures/is-fn.rs` * `tests/ui/async-await/async-closures/implements-fnmut.rs` * Async closures may be cloned * `tests/ui/async-await/async-closures/clone-closure.rs` * Ownership of the upvars when `AsyncFnOnce` is called * `tests/ui/async-await/async-closures/drop.rs` * `tests/ui/async-await/async-closures/move-is-async-fn.rs` * `tests/ui/async-await/async-closures/force-move-due-to-inferred-kind.rs` * `tests/ui/async-await/async-closures/force-move-due-to-actually-fnonce.rs` * Closure signature inference * `tests/ui/async-await/async-closures/signature-deduction.rs` * `tests/ui/async-await/async-closures/sig-from-bare-fn.rs` * `tests/ui/async-await/async-closures/signature-inference-from-two-part-bound.rs` </details> ## Remaining bugs and open issues * rust-lang#120694 tracks moving onto more general `LendingFn*` traits. No action needed, since it's not observable. * rust-lang#124020 - Polymorphization ICE. Polymorphization needs to be heavily reworked. No action needed. * rust-lang#127227 - Tracking reworking the way that rustdoc re-sugars bounds. * The part relevant to to `AsyncFn` is fixed by rust-lang#132697. ## Where do we expect rewriting `|| async {}` into `async || {}` to fail? * Fn pointer coercions * Currently, it is not possible to coerce an async closure to an fn pointer like regular closures can be. This functionality may be implemented in the future. ```rust let x: fn() -> _ = async || {}; ``` * Argument capture * Like async functions, async closures always capture their input arguments. This is in contrast to something like `|t: T| async {}`, which doesn't capture `t` unless it is used in the async block. This may affect the `Send`-ness of the future or affect its outlives. ```rust fn needs_send_future(_: impl Fn(NotSendArg) -> Fut) where Fut: Future<Output = ()>, {} needs_send_future(async |_| {}); ``` ## History #### Important feature history - rust-lang#51580 - rust-lang#62292 - rust-lang#120361 - rust-lang#120712 - rust-lang#121857 - rust-lang#123660 - rust-lang#125259 - rust-lang#128506 - rust-lang#127482 ## Acknowledgements Thanks to `@oli-obk` for reviewing the bulk of the work for this feature. Thanks to `@nikomatsakis` for his design blog posts which generated interest for this feature, `@traviscross` for feedback and additions to this stabilization report. All errors are my own. r? `@ghost`
Stabilize async closures (RFC 3668) # Async Closures Stabilization Report This report proposes the stabilization of `#![feature(async_closure)]` ([RFC 3668](https://rust-lang.github.io/rfcs/3668-async-closures.html)). This is a long-awaited feature that increases the expressiveness of the Rust language and fills a pressing gap in the async ecosystem. ## Stabilization summary * You can write async closures like `async || {}` which return futures that can borrow from their captures and can be higher-ranked in their argument lifetimes. * You can express trait bounds for these async closures using the `AsyncFn` family of traits, analogous to the `Fn` family. ```rust async fn takes_an_async_fn(f: impl AsyncFn(&str)) { futures::join(f("hello"), f("world")).await; } takes_an_async_fn(async |s| { other_fn(s).await }).await; ``` ## Motivation Without this feature, users hit two major obstacles when writing async code that uses closures and `Fn` trait bounds: - The inability to express higher-ranked async function signatures. - That closures cannot return futures that borrow from the closure captures. That is, for the first, we cannot write: ```rust // We cannot express higher-ranked async function signatures. async fn f<Fut>(_: impl for<'a> Fn(&'a u8) -> Fut) where Fut: Future<Output = ()>, { todo!() } async fn main() { async fn g(_: &u8) { todo!() } f(g).await; //~^ ERROR mismatched types //~| ERROR one type is more general than the other } ``` And for the second, we cannot write: ```rust // Closures cannot return futures that borrow closure captures. async fn f<Fut: Future<Output = ()>>(_: impl FnMut() -> Fut) { todo!() } async fn main() { let mut xs = vec![]; f(|| async { async fn g() -> u8 { todo!() } xs.push(g().await); }); //~^ ERROR captured variable cannot escape `FnMut` closure body } ``` Async closures provide a first-class solution to these problems. For further background, please refer to the [motivation section](https://rust-lang.github.io/rfcs/3668-async-closures.html#motivation) of the RFC. ## Major design decisions since RFC The RFC had left open the question of whether we would spell the bounds syntax for async closures... ```rust // ...as this... fn f() -> impl AsyncFn() -> u8 { todo!() } // ...or as this: fn f() -> impl async Fn() -> u8 { todo!() } ``` We've decided to spell this as `AsyncFn{,Mut,Once}`. The `Fn` family of traits is special in many ways. We had originally argued that, due to this specialness, that perhaps the `async Fn` syntax could be adopted without having to decide whether a general `async Trait` mechanism would ever be adopted. However, concerns have been raised that we may not want to use `async Fn` syntax unless we would pursue more general trait modifiers. Since there remain substantial open questions on those -- and we don't want to rush any design work there -- it makes sense to ship this needed feature using the `AsyncFn`-style bounds syntax. Since we would, in no case, be shipping a generalized trait modifier system anytime soon, we'll be continuing to see `AsyncFoo` traits appear across the ecosystem regardless. If we were to ever later ship some general mechanism, we could at that time manage the migration from `AsyncFn` to `async Fn`, just as we'd be enabling and managing the migration of many other traits. Note that, as specified in RFC 3668, the details of the `AsyncFn*` traits are not exposed and they can only be named via the "parentheses sugar". That is, we can write `T: AsyncFn() -> u8` but not `T: AsyncFn<Output = u8>`. Unlike the `Fn` traits, we cannot project to the `Output` associated type of the `AsyncFn` traits. That is, while we can write... ```rust fn f<F: Fn() -> u8>(_: F::Output) {} ``` ...we cannot write: ```rust fn f<F: AsyncFn() -> u8>(_: F::Output) {} //~^ ERROR ``` The choice of `AsyncFn{,Mut,Once}` bounds syntax obviates, for our purposes here, another question decided after that RFC, which was how to order bound modifiers such as `for<'a> async Fn()`. Other than answering the open question in the RFC on syntax, nothing has changed about the design of this feature between RFC 3668 and this stabilization. ## What is stabilized For those interested in the technical details, please see [the dev guide section](https://rustc-dev-guide.rust-lang.org/coroutine-closures.html) I authored. #### Async closures Other than in how they solve the problems described above, async closures act similarly to closures that return async blocks, and can have parts of their signatures specified: ```rust // They can have arguments annotated with types: let _ = async |_: u8| { todo!() }; // They can have their return types annotated: let _ = async || -> u8 { todo!() }; // They can be higher-ranked: let _ = async |_: &str| { todo!() }; // They can capture values by move: let x = String::from("hello, world"); let _ = async move || do_something(&x).await }; ``` When called, they return an anonymous future type corresponding to the (not-yet-executed) body of the closure. These can be awaited like any other future. What distinguishes async closures is that, unlike closures that return async blocks, the futures returned from the async closure can capture state from the async closure. For example: ```rust let vec: Vec<String> = vec![]; let closure = async || { vec.push(ready(String::from("")).await); }; ``` The async closure captures `vec` with some `&'closure mut Vec<String>` which lives until the closure is dropped. Every call to `closure()` returns a future which reborrows that mutable reference `&'call mut Vec<String>` which lives until the future is dropped (e.g. it is `await`ed). As another example: ```rust let string: String = "Hello, world".into(); let closure = async move || { ready(&string).await; }; ``` The closure is marked with `move`, which means it takes ownership of the string by *value*. The future that is returned by calling `closure()` returns a future which borrows a reference `&'call String` which lives until the future is dropped (e.g. it is `await`ed). #### Async fn trait family To support the lending capability of async closures, and to provide a first-class way to express higher-ranked async closures, we introduce the `AsyncFn*` family of traits. See the [corresponding section](https://rust-lang.github.io/rfcs/3668-async-closures.html#asyncfn) of the RFC. We stabilize naming `AsyncFn*` via the "parenthesized sugar" syntax that normal `Fn*` traits can be named. The `AsyncFn*` trait can be used anywhere a `Fn*` trait bound is allowed, such as: ```rust /// In return-position impl trait: fn closure() -> impl AsyncFn() { async || {} } /// In trait bounds: trait Foo<F>: Sized where F: AsyncFn() { fn new(f: F) -> Self; } /// in GATs: trait Gat { type AsyncHasher<T>: AsyncFn(T) -> i32; } ``` Other than using them in trait bounds, the definitions of these traits are not directly observable, but certain aspects of their behavior can be indirectly observed such as the fact that: * `AsyncFn::async_call` and `AsyncFnMut::async_call_mut` return a future which is *lending*, and therefore borrows the `&self` lifetime of the callee. ```rust fn by_ref_call(c: impl AsyncFn()) { let fut = c(); drop(c); // ^ Cannot drop `c` since it is borrowed by `fut`. } ``` * `AsyncFnOnce::async_call_once` returns a future that takes ownership of the callee. ```rust fn by_ref_call(c: impl AsyncFnOnce()) { let fut = c(); let _ = c(); // ^ Cannot call `c` since calling it takes ownership the callee. } ``` * All currently-stable callable types (i.e., closures, function items, function pointers, and `dyn Fn*` trait objects) automatically implement `AsyncFn*() -> T` if they implement `Fn*() -> Fut` for some output type `Fut`, and `Fut` implements `Future<Output = T>`. * This is to make sure that `AsyncFn*()` trait bounds have maximum compatibility with existing callable types which return futures, such as async function items and closures which return boxed futures. * For now, this only works currently for *concrete* callable types -- for example, a argument-position impl trait like `impl Fn() -> impl Future<Output = ()>` does not implement `AsyncFn()`, due to the fact that a `AsyncFn`-if-`Fn` blanket impl does not exist in reality. This may be relaxed in the future. Users can work around this by wrapping their type in an async closure and calling it. I expect this to not matter much in practice, as users are encouraged to write `AsyncFn` bounds directly. ```rust fn is_async_fn(_: impl AsyncFn(&str)) {} async fn async_fn_item(s: &str) { todo!() } is_async_fn(s); // ^^^ This works. fn generic(f: impl Fn() -> impl Future<Output = ()>) { is_async_fn(f); // ^^^ This does not work (yet). } ``` #### The by-move future When async closures are called with `AsyncFn`/`AsyncFnMut`, they return a coroutine that borrows from the closure. However, when they are called via `AsyncFnOnce`, we consume that closure, and cannot return a coroutine that borrows from data that is now dropped. To work around around this limitation, we synthesize a separate future type for calling the async closure via `AsyncFnOnce`. This future executes identically to the by-ref future returned from calling the async closure, except for the fact that it has a different set of captures, since we must *move* the captures from the parent async into the child future. #### Interactions between async closures and the `Fn*` family of traits Async closures always implement `FnOnce`, since they always can be called once. They may also implement `Fn` or `FnMut` if their body is compatible with the calling mode (i.e. if they do not mutate their captures, or they do not capture their captures, respectively) and if the future returned by the async closure is not *lending*. ```rust let id = String::new(); let mapped: Vec</* impl Future */> = [/* elements */] .into_iter() // `Iterator::map` takes an `impl FnMut` .map(async |element| { do_something(&id, element).await; }) .collect(); ``` See [the dev guide](https://rustc-dev-guide.rust-lang.org/coroutine-closures.html#follow-up-when-do-async-closures-implement-the-regular-fn-traits) for a detailed explanation for the situations where this may not be possible due to the lending nature of async closures. #### Other notable features of async closures shared with synchronous closures * Async closures are `Copy` and/or `Clone` if their captures are `Copy`/`Clone`. * Async closures do closure signature inference: If an async closure is passed to a function with a `AsyncFn` or `Fn` trait bound, we can eagerly infer the argument types of the closure. More details are provided in [the dev guide](https://rustc-dev-guide.rust-lang.org/coroutine-closures.html#closure-signature-inference). #### Lints This PR also stabilizes the `CLOSURE_RETURNING_ASYNC_BLOCK` lint as an `allow` lint. This lints on "old-style" async closures: ```rust #![warn(closure_returning_async_block)] let c = |x: &str| async {}; ``` We should encourage users to use `async || {}` where possible. This lint remains `allow` and may be refined in the future because it has a few false positives (namely, see: "Where do we expect rewriting `|| async {}` into `async || {}` to fail?") An alternative that could be made at the time of stabilization is to put this lint behind another gate, so we can decide to stabilize it later. ## What isn't stabilized (aka, potential future work) #### `async Fn*()` bound syntax We decided to stabilize async closures without the `async Fn*()` bound modifier syntax. The general direction of this syntax and how it fits is still being considered by T-lang (e.g. in [RFC 3710](rust-lang/rfcs#3710)). #### Naming the futures returned by async closures This stabilization PR does not provide a way of naming the futures returned by calling `AsyncFn*`. Exposing a stable way to refer to these futures is important for building async-closure-aware combinators, and will be an important future step. #### Return type notation-style bounds for async closures The RFC described an RTN-like syntax for putting bounds on the future returned by an async closure: ```rust async fn foo(x: F) -> Result<()> where F: AsyncFn(&str) -> Result<()>, // The future from calling `F` is `Send` and `'static`. F(..): Send + 'static, {} ``` This stabilization PR does not stabilize that syntax yet, which remains unimplemented (though will be soon). #### `dyn AsyncFn*()` `AsyncFn*` are not dyn-compatible yet. This will likely be implemented in the future along with the dyn-compatibility of async fn in trait, since the same issue (dealing with the future returned by a call) applies there. ## Tests Tests exist for this feature in [`tests/ui/async-await/async-closures`](https://github.com/rust-lang/rust/tree/5b542866400ad4a294f468cfa7e059d95c27a079/tests/ui/async-await/async-closures). <details> <summary>A selected set of tests:</summary> * Lending behavior of async closures * `tests/ui/async-await/async-closures/mutate.rs` * `tests/ui/async-await/async-closures/captures.rs` * `tests/ui/async-await/async-closures/precise-captures.rs` * `tests/ui/async-await/async-closures/no-borrow-from-env.rs` * Async closures may be higher-ranked * `tests/ui/async-await/async-closures/higher-ranked.rs` * `tests/ui/async-await/async-closures/higher-ranked-return.rs` * Async closures may implement `Fn*` traits * `tests/ui/async-await/async-closures/is-fn.rs` * `tests/ui/async-await/async-closures/implements-fnmut.rs` * Async closures may be cloned * `tests/ui/async-await/async-closures/clone-closure.rs` * Ownership of the upvars when `AsyncFnOnce` is called * `tests/ui/async-await/async-closures/drop.rs` * `tests/ui/async-await/async-closures/move-is-async-fn.rs` * `tests/ui/async-await/async-closures/force-move-due-to-inferred-kind.rs` * `tests/ui/async-await/async-closures/force-move-due-to-actually-fnonce.rs` * Closure signature inference * `tests/ui/async-await/async-closures/signature-deduction.rs` * `tests/ui/async-await/async-closures/sig-from-bare-fn.rs` * `tests/ui/async-await/async-closures/signature-inference-from-two-part-bound.rs` </details> ## Remaining bugs and open issues * rust-lang/rust#120694 tracks moving onto more general `LendingFn*` traits. No action needed, since it's not observable. * rust-lang/rust#124020 - Polymorphization ICE. Polymorphization needs to be heavily reworked. No action needed. * rust-lang/rust#127227 - Tracking reworking the way that rustdoc re-sugars bounds. * The part relevant to to `AsyncFn` is fixed by rust-lang/rust#132697. ## Where do we expect rewriting `|| async {}` into `async || {}` to fail? * Fn pointer coercions * Currently, it is not possible to coerce an async closure to an fn pointer like regular closures can be. This functionality may be implemented in the future. ```rust let x: fn() -> _ = async || {}; ``` * Argument capture * Like async functions, async closures always capture their input arguments. This is in contrast to something like `|t: T| async {}`, which doesn't capture `t` unless it is used in the async block. This may affect the `Send`-ness of the future or affect its outlives. ```rust fn needs_send_future(_: impl Fn(NotSendArg) -> Fut) where Fut: Future<Output = ()>, {} needs_send_future(async |_| {}); ``` ## History #### Important feature history - rust-lang/rust#51580 - rust-lang/rust#62292 - rust-lang/rust#120361 - rust-lang/rust#120712 - rust-lang/rust#121857 - rust-lang/rust#123660 - rust-lang/rust#125259 - rust-lang/rust#128506 - rust-lang/rust#127482 ## Acknowledgements Thanks to `@oli-obk` for reviewing the bulk of the work for this feature. Thanks to `@nikomatsakis` for his design blog posts which generated interest for this feature, `@traviscross` for feedback and additions to this stabilization report. All errors are my own. r? `@ghost`
Summary
This RFC unblock the stabilization of async closures by committing to
K $Trait
(whereK
is some keyword likeasync
orconst
) as a pattern that we will use going forward to define a "K-variant ofTrait
". This commitment is made as part of committing to a larger syntactic design pattern called the flavor pattern. The flavor pattern is "advisory". It details all the parts that a "flavor-like keyword" should have and suggests specific syntax that should be used, but it is not itself a language feature.In the flavor pattern, each flavor is tied to a specific keyword
K
. Flavors share the "infectious property": code with flavorK
interacts naturally with other code with flavorK
but only interacts in limited ways with code without the flavorK
. Every flavor keywordK
should support at least the following:K
-functions using the syntaxK fn $name() -> $ty
;K
-blocks using the syntaxK { $expr }
(and potentiallyK move { $expr }
);K
-traits using the syntaxK $Trait
;K
-flavored traits should offer at least the same methods, associated types, and other trait items as the unflavored trait (there may be additional items specific to theK
-flavor). Some of the items will beK
-flavored, but not necessarily all of them.K
-closures using the syntaxK [move] |$args| $expr
to define a K-closure;K Fn
traits.Some flavors rewrite code so that it executes differently (e.g.,
async
). These are called rewrite flavors. Each such flavor should have the following:🏠K<$ty>
defining theK
-type, the type that results from aK
-block,K
-function, orK
-closure whose body has type$ty
.🏠K<$ty>
is a placeholder. We expect a future RFC to define the actual syntax. (The 🏠 emoji is meant to symbolize a "bikeshed".)cK
-block, consumes a🏠K<$ty>
and produces a$ty
value.K
-function can be transformed to a regular function with aK
-flavored return type and body.K fn $name($args) -> $ty { $expr }
fn $name($args) -> 🏠K<$ty> { K { $expr } }
Binding recommendations
Existing flavor-like keywords in the language do not have all of these parts. The RFC therefore includes a limited set of binding recommendations that brings them closer to conformance:
K $Trait
as the syntax for applying flavors to traits, with theasync Fn
,async FnMut
, andasync FnOnce
traits being the only current usable example.🏠async<$ty>
that will meet the equivalences described in this RFC.Not part of this RFC
The Future Possibilities discusses other changes we could make to make existing and planned flavors fit the pattern better. Examples of things that this RFC does NOT specify (but which early readers thought it might):
async
can be used with traits beyond theFn
traits:Read
trait, it will be referred to asasync Read
, but the RFC does not specify whether to add such a trait nor how such a trait would be defined or what its contents would be.const Trait
ought to work (under active exploration):const
-flavored trait should beconst Trait
; it does not specify what aconst
-flavored trait would mean or when that syntax can be used.🏠K<$ty>
:async
, but the precise syntax still needs to be pinned down. RFC #3628 contains one possibility.Rendered