rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.97k stars 1.57k forks source link

RFC: introduce the flavor syntactic design pattern #3710

Closed nikomatsakis closed 3 weeks ago

nikomatsakis commented 1 month ago

Summary

This RFC unblock the stabilization of async closures by committing to K $Trait (where K is some keyword like async or const) as a pattern that we will use going forward to define a "K-variant of Trait". 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 flavor K interacts naturally with other code with flavor K but only interacts in limited ways with code without the flavor K. Every flavor keyword K should support at least the following:

Some flavors rewrite code so that it executes differently (e.g., async). These are called rewrite flavors. Each such flavor should have the following:

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:

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):

Rendered

camsteffen commented 4 weeks ago

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.

traviscross commented 4 weeks ago

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 async Fn{,Mut,Once} and AsyncFn{,Mut,Once}. The RFC had left this as an open question.

We had agreed, in later lang discussion, that we might have consensus on async Fn as that syntax if and only if we had agreement that we would generalize this somehow to other traits. This RFC is the outcome of that.

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 async Fn, I've realized more clearly that my reasons for that do not necessarily generalize to other traits. The Fn traits are special in many ways, and in fact, this specialness is what had originally prompted @compiler-errors to suggest that the async Fn syntax could be adopted without having to decide generally about async traits at all.

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 K OtherKTrait in these instances.

Looking beyond that, I sense that it's harder to apply this K Trait idea whenever the trait represents any kind of nontrivial finite state machine, and I suspect that is part of why we have so many question marks about the combination of this with traits like Read.

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 K Trait. For traits, it seems at least possible that we will just have a handful of special cases (e.g. for Fn, Default, Clone, etc.) or that we may have other available approaches for solving the relevant problems.

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 AsyncFn{,Mut,Once} bounds syntax, and I know from discussion that some other members of the team feel positively about this also. The author of RFC 3668, @compiler-errors, has also warmed to this.

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 async Foo, we are and will continue seeing AsyncFoo traits appear regardless. And that if we did later come up with some generalized mechanism, we could at that time manage the transition from AsyncFn to async Fn.

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 AsyncFn bounds syntax.

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.

PeterHatch commented 3 weeks ago

From the meeting notes:

I do find the idea that you can import one name and add flavors to it later quite compelling.

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.

I find it kind of annoying that you have to go import TryFrom everytime you want to use that instead of regular From, for example.

If I'm understanding correctly, under this proposal flavored traits are supposed to have the same members as the base trait, meaning try From would have a from method instead of try_from; so for any struct that implements both From and try From, I think you'd have to disambiguate which trait you were using every time? That seems much worse than needing to import TryFrom.

nikomatsakis commented 3 weeks ago

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 AsyncFn and continue our discussions.

Like TC, I ultimately do believe we should have some form of K-generics -- and I mean that in a fuller sense than this RFC, I think it's important that we are able to write combinator-like APIs (e.g., Iterator) that work whether or not you have ?, await, const, etc. Unlike TC, I am 100% confident that part of this will be "K-flavored" traits.

However, I don't think we need to stabilize async Fn syntax now to get there, and I would really like to see async Fn stabilized ASAP. I also feel that we should not use async Fn unless we are prepared to apply the async keyword to other traits, it will just be confusing for no reason. It seems better to start with AsyncFn -- if we want, we can always make that a (deprecated) trait alias in the future.

rpjohnst commented 3 weeks ago

I don't think this (discussion about more general "K-flavoring") really changes anything about the idea that the Fn trait sugar was designed to match fn item syntax.

More specifically: regardless of whether we start with AsyncFn or async Fn, we are extending that sugar. I would thus argue for async Fn over AsyncFn even if we never added async to any other traits. And this line of discussion was already covered pretty heavily in the async closures RFC.

Diggsey commented 3 weeks ago

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.