--- title: "Design meeting 2024-10-23: Flavor RFC" tags: ["T-lang", "design-meeting", "minutes"] date: 2024-10-23 discussion: https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202024-10-23 url: https://hackmd.io/w2-etPU7RouzxJ6GJ1fbSg --- # 2024-10-23: Flavor RFC [RFC #3710]: https://github.com/rust-lang/rfcs/pull/3710 [#67792]: https://github.com/rust-lang/rust/issues/67792 [RFC #3628]: https://github.com/rust-lang/rfcs/pull/3628 [RFC #3668]: https://github.com/rust-lang/rfcs/pull/3668 [RFC #243]: https://github.com/rust-lang/rfcs/pull/243 [RFC #2071]: https://github.com/rust-lang/rfcs/blob/master/text/2071-impl-trait-existential-types.md [asyncfg]: https://rust-lang.github.io/rust-project-goals/2024h2/async.html [droporder]: https://github.com/rust-lang/rust/blob/0b16baa570d26224612ea27f76d68e4c6ca135cc/compiler/rustc_ast_lowering/src/item.rs#L1179-L1210 [WCIF]: https://journal.stuffwithstuff.com/2015/02/01/what-flavor-is-your-function/ ## Goal of the meeting [RFC #3710] describes what nikomatsakis believes to be the consensus we reached as a team in our meetup. The goal of this meeting is to review the scope of that document and in particular the concerns raised on the thread. Assuming the team remains aligned nikomatsakis plans to propose `fcp merge`. ## Summary of RFC #3710 [RFC #3710] defines the "flavor" design pattern. This is a syntactic design pattern and not a language feature. The RFC does not describe or commit to any form of "flavor generics" but it is intended to leave space for the possibility of such a feature. What follows is the "summary" section of the RFC. 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: * `K`-functions using the syntax `K fn $name() -> $ty`; * `K`-blocks using the syntax `K { $expr }` (and potentially `K move { $expr }`); * `K`-traits using the syntax `K $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 the `K`-flavor). Some of the items will be `K`-flavored, but not necessarily all of them. * `K`-closures using the syntax `K [move] |$args| $expr` to define a K-closure; * Closures implement the `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: * A syntax `🏠K<$ty>` defining the `K`-type, the type that results from a `K`-block, `K`-function, or `K`-closure whose body has type `$ty`. * The `🏠K<$ty>` is a placeholder. We expect a future RFC to define the actual syntax. (The 🏠 emoji is meant to symbolize a "bikeshed".)c * A "do" operation that, when executed in a `K`-block, consumes a `🏠K<$ty>` and produces a `$ty` value. * The property that a `K`-function can be transformed to a regular function with a `K`-flavored return type and body. * i.e., the following are roughly equivalent (the precise translation can vary so as to e.g. preserve drop order): * `K fn $name($args) -> $ty { $expr }` * `fn $name($args) -> 🏠K<$ty> { K { $expr } }` ## Not part of this RFC The [Future Possibilities](#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): * Any form of "effect" or "flavor" generics: * Flavors in this RFC are a pattern for Rust designers to keep in mind as we explore possible language features, not a first-class language feature; this RFC also does not close the door on making them a first-class feature in the future. * Whether or how `async` can be used with traits beyond the `Fn` traits: * For example, the RFC specifies that **if** we add an async flavor of the `Read` trait, it will be referred to as `async 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. * How `const Trait` ought to work ([under active exploration][#67792]): * The RFC only specifies that the syntax for naming a `const`-flavored trait should be `const Trait`; it does not specify what a `const`-flavored trait would mean or when that syntax can be used. * What specific syntax we should use for `🏠K<$ty>`: * We are committed to adding this syntax at least for `async`, but the precise syntax still needs to be pinned down. [RFC #3628][] contains one possibility. ## Summary of the discussion thread The primary topics on the discussion thread have been as follows. ### Non-mechanical mechanisms There was initially a misunderstanding that the RFC is proposing that `async $TraitName` be derived via some mechanical or automated means. In fact the RFC does not say that, it only specifies that the `K`-flavor of trait have a superset of the members of the unflavored version of the trait, not how the. From the FAQ: > That is not yet clear. The only flavored trait currently RFC'd is the `async` flavor of the `Fn` trait. It is not clear whether we will ever add an `async` flavor for the `Read` trait or what language mechanism we would use to do so. It could be automatically derived from `Read` but it could also be a divergent definition. > > However, the RFC does give some guidance for flavored traits. Specifically, it says that a flavored trait should offer *at least* the same members as the unflavored version, some of which may themselves now be flavored. The `async FnOnce` trait is a good example of this. Like the ordinary `FnOnce` trait, it offers a `call_once` method (but flavored `async`) and an `Output` associated type. As an implementation detail, it has additional members, like the `CallOnceFuture`; these are not currently exposed to users but they could be in the future. > > The reason that we specify that flavored traits have *at least* the same members as unflavored ones is to preserve the invariant that unflavored code can be ported to a flavor by "just adding the flavor keyword". If there is existing unflavored code using the trait, you should be able to "flavor" it easily. This would not be possible if the flavored version of the trait lacked some of the features or mechanisms of the original (unless of course the existence of those features is directly in contradiction with the purpose of the flavor, e.g., a panic-free flavor would not include a panic-free flavored method that is guaranteed to panic). We then discussed a hypothetical alternative in which users could explicitly declare the "async flavor" of a trait, perhaps with a syntax like:; ```rust trait async Fn { } ``` This led to some questions about how such a flavor would be imported. This is obviously not part of the RFC, but my response was that I would assume that users would continue to import `Fn` (the actual identifier here). In other words, a `trait async X` declaration is not declaring a *separate trait* `X` but rather the `async`-flavor for the trait `X`. Like an `impl`, this is not something one can import, but rather a connection made by the type system. ### The syntax `async Read` implies a "primary" read trait for async The RFC only commits us to adding an `async` flavor of the `Fn` trait, but it does state that any future "async-flavored" traits will be referred to `async TraitName` and not (e.g.) `AsyncTraitName`. The point has been raised that this syntax, no matter its semantics, already implies that there will be some form of *primary* "async version of Read", and that this is not a good idea. GoldsteinE put it well in [this comment](https://github.com/rust-lang/rfcs/pull/3710#issuecomment-2414957110): > Special syntax for flavours makes sense when there’s an obvious one-to-one correspondence between flavoured and unflavoured variants. That’s true for `const` and `unsafe`, but that’s _not_ true for `async`. By commiting to giving `AsyncRead` a special syntax, we commit to choosing one of the flavoured variants and implicitly encouraging it at expense of the other. That’s a drawback of having a special syntax for `AsyncRead` and any other trait that can have multiple incompatible definitions, which is not mentioned or in any way explored in the RFC text. The following is a draft FAQ not yet present in the RFC: The RFC does not commit to `async Read`. There are good arguments in favor of having an async-flavor of `Read`, but (as the question indicates) there are also some counterarguments. While this RFC does not settle the question, it does narrow the options somewhat to the following: * We can add an `async Read` trait. The trait must have a superset of `Read`'s functionality but with some members made `async`. In this scenario, nothing precludes us from adding other traits to target completion-based scenarios, they simply need to have a different name. * This is compatible with e.g. nrc's [proposal](https://github.com/nrc/portable-interoperable/tree/master/io-traits#read-proposal) which included an ["async flavored `Read`" trait that closely follows sync read](https://github.com/nrc/portable-interoperable/tree/master/io-traits#read-proposal) and two subtraits, `ReadyRead` and `OwnedRead`, that allow for being more specific. Note that nrc's async-flavored read also includes methods like `as_ready` not found in the sync `Read`. We would have to decide whether to make distinct sync-and-async flavors of `ReadyRead` and `OwnedRead` or to have those traits be async-only. * We can never have an async flavor of `Read` but instead add async-only traits with different names (e.g., `BorrowedRead` and `CompletionRead`). We can comment on `Read` that there is no async flavor of `Read` because, when writing async code, one really ought to pick between the two varieties. Choosing between these two options can be done in the future. We should take into account "clarity of purpose" and "correctness by construction" [Rust design goals](https://tmandry.gitlab.io/blog/posts/the-main-thing/) when doing so. Note the expectation that **not every trait will have an async flavor**. Many traits will just have the "default" flavor and that flavor may include async methods. ### "Just add async" is a mirage, as is the correspondence between `async` and `const` The premise of generalizing `async Fn` to apply to other traits like `async Read` is that people will be able to move code from sync-to-async by just adding `async`. As a variation on the previous point, PeterHatch [points out](https://github.com/rust-lang/rfcs/pull/3710#issuecomment-2430954764) that this may be true in simple cases but that the reality is more complex: > 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. No response has been drafted yet because I only saw this right now! ### Keyword syntax has practical disadvantages Other concerns about `async Fn` as a syntax: * ### Choice of name The RFC originally used the term "color". @clarfonthey expressed a preference for the term "effect" or potentially "affect", arguing that this is what the term has precedent both amongst current Rust WGs and academic material. [[1](https://github.com/rust-lang/rfcs/pull/3710#issuecomment-2405647443), [2](https://github.com/rust-lang/rfcs/pull/3710#issuecomment-2405739686), [3](https://github.com/rust-lang/rfcs/pull/3710#issuecomment-2415177184)]. The FAQ outlines the following criteria for selecting a name: > * **Memorable and specific:** A term that is too common and generic can be confusing. This can be because the word is used too much for too many things or because the word is just such a common English word that it doesn't *sound* like a specific thing. > * **Grammatically flexible:** It's good to have a term that can be used in multiple ways, for example as an adjective, noun, etc. > * **Approachable:** When I say "jargon" what I often mean is that it's a word that kind of has a formidable feeling, like "monad". When you hear the term you instantly feel a little bit excluded and less smart (or, alternatively, when you know the term well and use it correctly, you feel a bit smarter). That seems like a problem to me. > * **Familiar:** it's good to use a term that people find familiar and that gives them the right intuitions, even if it's not aligned on every detail. Based on this criteria, I opted for the "flavor" pattern. This name is not binding in that it doesn't correspond to anything we expect to use in the language as of yet, but it is likely to appear in documentation or explanatory material, so it's worth thinking seriously about. Other alternatives considered (from the FAQ): > **Effect** is reasonably memorable and specific but it not familiar nor approachable to people in conversation. It is also not grammatically flexible. For example, the RFC frequently talks about *`K`-flavored traits*, but it's not clear what the adjective form of an effect is (`K`-effected traits?). > > **Color** was initially chosen as a playful allusion to the ["What color is your function"?][WCIF] blog post. Color is approachable but such a common word in conversation that readers may not realize it is a "term of art". It is also widely used in some domains, such as GUIs, making it a poor choice for keyword (not that we are proposing a keyword). Also, while color is relatively grammatically flexible, it can still be awkward. For example, we sometimes talk about a specific "flavor" of a trait, and saying "the async color of the Trait" doesn'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 some common phrasings could have undesirable connotations. The FAQ doesn't discuss it (perhaps it should), but there are some other concerns about effect. Its connection to academia gives it a specific meaning that is not suitable for everything we use -- `unsafe`, for example, feels very much like a "flavor" to users (good litmus test: would it be usable to have an `unsafe Fn` trait or `unsafe` closures?), and yet it isn't well described by effects as there is no function that one calls to "be" unsafe. (There are counterarguments too, the point is mostly that it is controversial, not to litigate whether or not unsafe could be considered an effect precisely.) ### What about `mut`, is that a flavor? From the FAQ: > The `mut` keyword does not qualify as a flavor per the definition in this RFC because it cannot be applied to functions or blocks. As such, it's not clear how one would extend `mut` to apply to other locations like traits or closures. > > However, it *is* true that `mut` is part of what often feels like "3 modes" in Rust: `self`, `&self`, and `&mut self`. It is possible to imagine creating some sort of flavor such that e.g. the `Fn`, `FnMut`, and `FnOnce` traits, for example, would be flavored variants of a single "callable" trait. > > Another similar split is `Send` vs not `Send`. We don't have a keyword for this, but especially in async code it is a very real split. ## An alternative summary I continue to think that the RFC should be accepted. However, I also continue to think I haven't *nailed* the framing of the RFC. I will take one last attempt here and I would like feedback on whether this framing is superior. The primary goal of this RFC is to commit to using `async Fn` as the syntax for async closures (as proposed in [RFC #3668][]) and to adding some form of syntax for 'async flavored types' similar to what is proposed in [RFC #3628][]. This "async flavored type" notation, currently denoted with the placeholder syntax `🏠async<$ty>`, would be equivalent to `impl Future<Output = T>` (and hence its precise meaning would be dependent on where it appears, just as `impl Future` can expand in different ways). The motivation for comitting to both `async Fn` and `🏠async<$ty>` syntaxes together is to create a coherent design pattern around `async` that allows the user to understand all aspects of the "async flavor" of Rust as a unit. The goal is that taking sync code and making it async can be done by adding "async" and "await" in the appropriate places without hitting places where other abstractions "leak through". The second goal of this RFC is to make strong suggestions for the syntax of future features that are under consideration. Based on `async`, the RFC defines a *flavor design pattern*. The flavor design pattern is a series of syntax recommendations and transformations that can be applied to any "flavor keyword" `K`. A *flavor keyword* `K` is some keyword that can be applied to a function or a block, like `K fn foo()` or `K { /* something */ }`, and which has the "infectious" property, meaning that code with flavor `K` interacts naturally with other code of the same flavor, but only in limited ways with code of other flavors. Beyond `async`, other examples of existing flavor keywords are `const` and `unsafe`. (There are also other "flavor-like" things in Rust that do not have keywords, like functions that operate on `&T/&mut T/T`; these are not described by this RFC directly.) Based on the flavor design pattern, the RFC recommends that we use `const $Trait` as the syntax to indicate a version of `$Trait` in which some or all members are made `const`-flavored and that we use `async $Trait` as the syntax for future "async flavors" of traits that we may decide to add (e.g., `async Read`). The RFC does NOT define any such traits nor propose any kind of means to define them, mechanical or otherwise; the only criteria is that `K $Trait` should have a superet of the members of `$Trait` but where some have been made `K`-flavored (`async Fn` as defined in [RFC #3668] meets this definition). The RFC includes a "future possibilities" section that describe various things we could do, such as what `const $Trait` might mean, how `unsafe Trait` could work (and might not), and the possibility of "flavor generics" (aka, effect generics or keyword generics). **None of those features are proposed in this RFC.** --- # Discussion ## Attendance - People: TC, pnkfelix, tmandry, Josh, nikomatsakis, yosh, Xiang, Nadri ## Meeting roles - Minutes, driver: TC ## High level NM: I want to move this forward to move async closures forward. So I want to focus on concerns that would prevent people from checking the box on this RFC. ## `try` as a trait flavor tmandry: Our previous discussions of this pattern included `try`, but I don't fully see how it fits into one of the minimum requirements of a flavor: That there can be a `try Trait` flavor of any trait: > Every flavor keyword `K` should support at least the following: > > ... > * `K`-traits using the syntax `K $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 the `K`-flavor). Some of the items will be `K`-flavored, but not necessarily all of them. As a rough sketch, consider `try Iterator`. Would we say that the existing methods with `try_` variants today, like `collect`, `find`, `reduce`, etc. are rewritten to have the same signature as the existing `try_` variants, or would those just be added on top of the existing vanilla methods? Josh: I think `try` is a *family* of flavors. In some cases, a function will want to support *any* `try` flavor (e.g. "pass me a closure of any flavor and I'll be the same flavor"); in some cases a function will have one *specific* `try` flavor because it wants to return a specific error type. (And in some cases I think people may want *multiple* `try` flavors.) That complexity is one reason `try` isn't in the baseline proposal. It should be *possible* to write `try Trait`, but in many cases you won't want a whole `try` variant of the *Trait*, you'll want a specific *function* that works with `try` and not `try`. (e.g. sometimes you'll want `try Iterator` but sometimes you'll want a regular `Iterator` that has a method that supports optional flavors like `try`.) pnkfelix: Wait, where is the min req stated that *any* trait must admit a variant for a given flavor? I thought it was up to specific traits to decide whether they made sense to provide flavor? tmandry: It's not.. it's saying that individual traits should be able to define a try variant. I'm trying to work out what it means in a practical example and answer the question of whether we want that. pnkfelix: Mind referencing the specific point in the text for "there can be a `try Trait` flavor of any trait" so I/we can help better? :heavy_check_mark: pnkfelix: (thanks. I guess the text there admits different interpretations.) yosh: for `try` + `Iterator` I'd expect something similar to the [fallible-iterator crate](https://docs.rs/fallible-iterator/latest/fallible_iterator/index.html). But probably using `Try` or `Residual` rather than hard-coding `Result`. That's probably not a huge leap tho! --- NM: What I imagine `trait try Iterator` would look like is this: ```rust trait try Iterator { type Item; fn size_hint(&self) -> SizeHint; try fn next(&mut self) -> Option<Self::Item>; fn map<U>(self, op: impl try FnMut(Self::Item) -> U) -> impl try Iterator<Item = U>; // ... and so on for all combinators ... try fn for_each(&mut self, op: impl try FnMut(Self::Item)) -> (); } ``` JT: there are two ways that "try" could interact with e.g. map or for-each. You want `try Iterator` to have a `try fn next`. But on *any* `Iterator`, you want the ability to pass a `try` closure (or an `async` closure) to `map` or `for_each` and have it propagate that effect. NM: IS the heart of this whether *iteration can fail* or *iterator can produce values that have failed*? The latter is not, to my mind, a "try iteration" -- it is what we do quite often. JT: you also want the ability to pass a closure that might fail with a try (or use `async` or any other flavor), whether or not you have an `Iterator` that itself has a flavor. TC: I'm still a bit unsure there needs to be a separate trait definition for a `try Iterator`. Setting aside backward compatibility for a moment, this seems more like a case for refinement, e.g.: ```rust trait Gen { type Output; type Item; fn next(self: Pin<&mut Self>) -> Self::Output; } ``` NM: I'm trying to hold off on drilling into the *details* of `Iterator` but I do think there is a very comparable thing here that has to do with whether you have a "flavorful iteration" or an "iteration that produces flavorful results". e.g. I think that having a "try Iterator" means that *iteration (as a whole) can fail* just like "async Iterator" would mean *iteration must be awaited*. But having an `Iterator<Item = try<T>>` or `Iterator<Item = async<T>>` would be an iteration that can produce things that have failed or which must be awaited. Nadri: `Iterator` is quite special, what other examples to we have? Various ppl: `Default`, `Clone`, `Into`, (maybe) `Drop` work well with `try`, `async`, `const` FK: In this RFC, we are not talking about how you define a given variant, right? Just a naming convention? Can we think of it as a namespacing construct? JT: I think Iterator is a great example for the full generality; I agree that the simple cases are good too, but Iterator is a good example to examine all the cases and see how they all can make sense. Sometimes you want `try Iterator<Item = T>`, sometimes you want `Iterator<Item = try<T>>`, sometimes you want `Iterator<T>` but you want to call `for_each` or `map` with a `try` closure and have it return a `try`, and sometimes you may want to combine those. Nadri: sounds similar to the case of `async Read`. NM: I found that the flavor terminology helped me to talk about this, side note -- e.g., a "flavored Iterator" vs an "Iterator of flavored values". ## Do keywords add more complexity than we realize? tmandry: Building on a couple of outstanding proposals, consider the following function signature... ```rust fn debug_items( iter: &pin mut impl async pin Iterator<Item = impl Debug> ) ``` There's a lot going on here, and we haven't brought in `const` or `unsafe`. Note: Maybe we want `pin Iterator` to actually be called `Generator`, since `pin` doesn't fit the definition of a flavor in this RFC. Josh: I think we definitely don't have clear consensus yet on `pin` being something we want to treat as a flavor. But more generally... I agree that stacking many keywords in a row results in some degree of confusion. However, I think it's *less* confusing on types (e.g. `async Trait`) than it is on `fn`: I think users will have some degree of confusion about whether they want `try async fn` or `async try fn`, and which order the types occur in the return type. I think it'll be *less* confusing and more obvious if users write return values like `try<🏠E, async<T>>` or `async<try<🏠E, T>>`; those have an obvious meaning and ordering. --- NM: I want to focus on this question, because really all the RFC is saying is "hey we like keywords", so if we don't like having a series of keywords...that's a problem. I would rather we look at `try` than `pin` because I think `pin` adds some orthogonal considerations. This is the kind of example that both super well motivates this and which is a bit frightening: ```rust fn debug_items( iter: &mut impl async try Iterator<Item = impl Debug> ) fn debug_items( iter: &mut impl AsyncTryIterator<Item = impl Debug> ) ``` TM: I'm interested also in the transition around the keyword `impl` -- before that, we have properties of the reference (`mut`), afterwards, properties of the trait, is that something we expect users to pick up on? How do we teach that? FK: what I expect to be part of the story is that IDEs will be able to do much better with the first version. The second version is going to have a whole slew of options. That to me is an argument that the fear of keywords may be misplaced, tooling may be able to help a lot better. JT: I do think there'll be some amount of confusion when stacking a pile of keywords in front of fn. I don't think it's going to be confusing on traits/types, it'll be obvious what order it applies in when applied to a trait or type, but if I write `async try fn` vs `try async fn`, what order do I remove those from the fn and apply those to the return value? I think `fn func() -> async<try<T>>` will be more obvious in that respect. TC: By order, you mean like `try<async<Fn>>`, that kind of thing? JT: Right, do you have a `Result<impl Future<Output = T>, ...>` or an `impl Future<Output = Result<T, ...>>`? NM: I don't know that the ordering will be significant here. It could be, but I don't know that it should be. The designs I favor don't include ordering. But also, I think what FK is saying is really about there being an advantage of having *first class* notion of flavors. JT: (I'd be very surprised if we *don't* support both orderings; both orderings make sense.) FK: Yes, and I would expect e.g. rustdoc to support it. (E.g. via presenting the flavored variants of a trait as a sublist under the trait itself in the overview panel) NM: Good point, I definitely have had rustdoc integrating flavors in mind, and as you were saying it I realized I never really described it. I like the term "namespacing", that is a good word I should adopt. ```rust fn debug_items( iter: &mut (impl (async try Iterator<Item = impl Debug>)) ) ``` ```rust fn debug_items( iter: &mut impl [async, try] Iterator<Item = impl Debug> ) ``` ```rust fn debug_items( iter: &mut impl async+try Iterator<Item = impl Debug> ) ``` ```rust fn debug_items( iter: &mut impl async Iterator<Item = impl Debug> ) ``` ```rust fn debug_items( iter: &mut impl Iterator[async]<Item = impl Debug>, iter: &mut impl Iterator[async try]<Item = impl Debug> ) ``` TM: Given the two options of keyword vs camelcase, just have to weigh the tradeoffs. I do find the idea that you can import one name and add flavors to it later quite compelling. 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. IDE support is kind of a point in favor but at the same time we can always teach the conventions like `Async` as a prefix? TC: I have to say that `impl Iterator[async, try]` is not that bad. JT: I think `async gen` is a notably interesting example. `async gen` is kind of a separate flavor onto itself that might not just be a combination of async+gen. But `async try` and `try async` make sense as distinct flavors and many of them make sense in different combinations. `async gen` is unusual in that regard. Theoretically you might want a "gen async" that generates asyncs, but it's not obvious if `async gen`is just an `async` of `gen`. NM: What we're deciding on here mostly is a first-class notion of flavors rather than just a convention, and that has value. When something works 90% only, it's often suprisingly different to support it in things like IDEs. Most of the time, syntax highlighting is present, and it does affect how one parses things in one's mind. We should think too about how `rustdoc` presents these things. NM: Also I continue to want to put a pin in the question of ordering, I prefer to have it be a set, but either way we need a string of keywords. Yosh: Swift added first-class support for displaying overloads, that might be a great starting point for us to reference. NM: Nice. Maybe I can steal some screenshots to convey the idea. Yosh: Vera (Misdreavus) came back with an example: [Searchable (Swift Doc)](https://developer.apple.com/documentation/swiftui/view/searchable(text:placement:prompt:)). ## `K`-block syntax with explicit type? Josh: > `K`-blocks using the syntax `K { $expr }` (and potentially `K move { $expr }`) I think we should mention the possibility of `K<ty> { $expr }` blocks, to specify the return type. I also hope that we can find a way to avoid the stutter in `fn func() -> K<ty> { K { ... } }`. NM: I would be happy to extend the discussion of adding a syntax for flavored types, I can see it being a nice addition. Also this discussion has reinforced the value of having a syntax for flavored types, being able to distinguish a *flavored iterator* from an *iterator over flavored types* feels really useful. JT: The thing that might not be obvious is that we should use the *same* syntax for "apply flavor to type" (e.g. `async<T>`) and "flavor block with an explicit type" (e.g. `async<T> { ... }`), rather than having the two be gratuitously different. TC: There is ascription in the RFC. Search for `async ->`. ## Choice of name > This name is not binding in that it doesn't correspond to anything we expect to use in the language as of yet, but it is likely to appear in documentation or explanatory material, so it's worth thinking seriously about. TC: If we have time, let's talk about this. Perhaps I'm concerned that it may be reductive to try to group: - `unsafe` - `const` - `async` ...into one thing, as there are some critical differences, such as the opposite "polarity" of `const`, and that by trying to reduce these so far, that's pushing us into a kind of fuzzy and jargony name. JT: There are two distinct points here that I think we should handle separately. One, do we pick a name meaningful in the literature? Two, what non-meaningful name do we pick? I think it is correct to not use effect, as I think that brings a lot of preconceptions that we might not want to conform to. Once we've decided to use a non-meaningful name, flavor feels like as good a name as any. TC: I think people's comments go to the first part. The main thrust is "why are you picking a meaningless name when there is already a name established in the literature"? The things that fall outside of that, maybe they shouldn't be in this category at all? Why pick a name that doesn't have meaning and then try to force things into it? I'm sympathetic that trying to pick a name that is not effect is maybe a bridge too far, maybe the constraints of the medium in picking a name that has established meaning is a useful constraint? Nadri: I feel like part of the point here is that we are not trying to do the mechanistic derivation. The try flavor of `Read` doesn't read to me "read that does the effect `try`"-- there are choices in doing the flavor thing. In my mind that reads as a good use of "not the technical word". TC: In what sense don't we want that? Nadri: We don't want that `try Read` is a trait that is automatically defined from `Read` by automatically adding `try` to all its methods? JT: Not sure if we don't want it, but we are not committing to it. I think it might be useful to be able to have a way to say "if you'd like have a flavor of this trait, here is the methods that will get that flavor". I don't think we should precommit to *not* doing this, just as I don't think we should precommit to *doing* it. TC: I don't really associate effects as doing some automatic rewriting, no languages have it that I know of. The effects are more like "bounds", communicating what's true or what has to be true. NM: Personally, (a), I do feel that unsafe is an effect, and I think the counterarguments are wrong. But also (b) either way, anything that makes you want to copy and paste a trait and add variants (which definitely includes `unsafe`), feels like a *thing*, a pattern, and I like having a name for it. I'm also curious TC what you think of some of the other arguments, mostly the ability to use "flavor" in so many *ways* (e.g., flavored type) that feels really natural and useful. I admit to some bias that if you use academic terminology you make everything less accesible--not always, but the batting average is very poor. TM: I'd like some clarity about whether we consider `Send` to be a flavor. Feels relevant. Also, there was some support for this syntax, should we do a straw poll? ```rust fn debug_items( iter: &mut impl Iterator[async]<Item = impl Debug>, iter: &mut impl Iterator[async, try]<Item = impl Debug> ) ``` Josh: Kinda like the *concept* of putting flavors into some kind of bracket, but the use of square brackets is breaking my brain: in type syntax, square brackets are arrays, and shouldn't be something else. | | `Iterator[async, try]` | `Iterator[async try]` | `async try Iterator` | Other | |--------------|------------------------|------------------------|----------------------|-------| | nikomatsakis | no¹ | "maybe" | +1 | +1 | | tmandry | +0.5 | +0.75 | +1 | | scottmcm | | | | | pnkfelix | | | | | Josh | -0* | -0* | +1 | depends on the proposal | | TC | +1 | "maybe" | +1 | | Nadri | -0* | -0* | +1 | | Yosh | +0 | +0 | +1 | - | | Xiang | | | +1 | Josh: The `-0*` here is "I might like some different kind of grouping syntax, just not square brackets because those mean 'array'". Wouldn't block, but ewww. no¹ — I don't like the commas, in particular, I feel like it is ... making multiple arguments instead of 1. Nadri: `*` here is: I want bracket syntax (and the name "effect") for "real" effects e.g. algebraic effect with effect generics etc NM: FWIW, I think flavors *can* be compatible with algebraic effects, but let's discuss separately =) Nadri: let's :) (The meeting ended here.) ## Nit about `move` > * `K`-blocks using the syntax `K { $expr }` (and potentially `K move { $expr }`); TC: It's a nit as far as this RFC goes, but I've come to think that we put `move` in the wrong place with `async`, and that we should have been consistent with closure syntax. I.e.: ```rust move || {} move async {} ``` This matters when we think of how to do ascriptions, e.g.: ```rust move || -> u8 { 0 } move async -> u8 { 0 } ``` I would like to fix this. Josh: I feel that this is correlated with the syntax used for ascriptions. If writing `async -> T`, I agree that that doesn't work well with `async move` (`async -> T move` and `async move -> T` both seem wrong). But if writing `async<T>`, I think the current ordering is fine. In any case, we've already shipped `async move`, so at best we can only *add* support for the other order. And even if we thought the other order was more correct, we shouldn't confuse people by having new constructs only support the new order and not the old one; e.g. `newflavor move` should still work because `async move` does and it'd be a confusing inconsistency otherwise. TC: We e.g. could fix this when switching to `use`, if we did that. Consider also we need to make choices about closure versions: ```rust move async gen || -> u8 { 0 } async move gen || -> u8 { 0 } // ? async gen move || -> u8 { 0 } // ? async gen || move -> u8 { 0 } // ? ``` (Assuming for the sake of this example we had non-`()` `gen` return types.) ## Does everyone agree that `AsyncRead` would be an anti-pattern? pnkfelix: Just to check my comprehension: Some reviewers said that they objected to `async Read` on the basis that there should not be **any** primary async variant of the `trait Read`. pnkfelix: So, is it fair to conclude: "If we eventually conclude that there exists a *sensible*+*primary* async variant for `trait Read`, then it should be denoted by `async Read`; and if we eventually conclude that there does not exist any such sensible+primary variant, then it should have some name other than `AsyncRead`" (and therefore in all universes, `trait AsyncRead` is an anti-pattern). yosh: I believe that is a sensible conclusion, yes. This RFC does not attempt to answer whether certain translations make sense, only what their syntax should look like if we decide to introduce them. pnkfelix: I raise this mostly to try to say "maybe there exist other potential flavors that could help motivate the proposed pattern" -- (unfortunately, I worry that *all* hypothetical async flavors, other than `async Fn{,Mut,Once}`, may fall into the same trap in which `async Read` has fallen). yosh: Can you clarify what trap that is? pnkfelix: The trap being that there does not exist a primary async flavor for Read; I'm worried that all (most?) traits will end up in same boat for async. Nadri: we have `Default`, `Clone`, `Into` etc as examples that would be straightforwardly `async`. Generally I expect traits that manipulate data in a "functional" way to have a straightforward extension to `async`/`try`. ## Send as a flavor tmandry: From the FAQ it isn't clear to me whether we would consider `Send` as a flavor... we probably want a `Send Trait` where `Trait` has `async fn` methods. ## mut as a flavor tmandry: Let's talk about it. Josh: It does have some of the properties of flavors: if you want to *optionally* be mut, you have to act as if you might be mut (so you can't have multiple borrows of the same thing that might be mut).