--- title: "RTN syntax options" tags: ["T-lang"] date: 2024-02-29 discussion: https://rust-lang.zulipchat.com/#narrow/stream/410673-t-lang.2Fmeetings/topic/RTN.20meeting.202024-03-04 url: https://hackmd.io/KPRLXXmISoWgX38alWUEnA --- # RTN syntax options This doc lays out the RTN syntax options and some of the arguments for or against a particular choice. There is one subtle point. The current decision is focused on a particular use case (bounding the return for all values of generic parameters and parameter types), but the choice also impacts what we might do for potential future use cases (e.g., bound the return type for specific values of generic parameters or parameter types; specifying which functions are const), so the examples given cover those use cases as well (and in some cases multiple possibilities). **But it's important to realize that we are not deciding on those future use cases yet -- there is still room to debate and/or find alternatives, and we may decide not to solve those problems at all.** ## Status quo: `T: Trait<method(): Send>`, `T::method(): Send` As our baseline case, we will discuss the syntax that is currently (partially)[^fn] implemented, along with some of its strengths and weaknesses. This will also introduce a sample program that we can port to future syntaxes. [^fn]: Only the "associated type bounds" form is currently implemented. Given the following trait and top-level function... ```rust trait Database { fn items(&self) -> impl Iterator<Item = Item>; fn items_with_key<K>(&self, K) -> impl Iterator<Item = Item> where K: IntoKey; fn process_items<T>( item_processor: &mut impl ItemProcessor<Output = T>, context: &[str], ) -> impl Iterator<Item = Vec<T>>; } fn generate_items(max_id: usize) -> impl Iterator<Item = Item> {} ``` One could write functions that bound the return type of the `items` method as follows: ```rust // "Associated type bounds"-form (this is the only form currently implemented) fn zip_items<D>(d1: &D, d2: &D) where D: Database<items(): DoubleEndedIterator>, {} // "Independent type"-form fn zip_items<D>(d1: &D, d2: &D) where D: Database, D::items(): DoubleEndedIterator, {} // "Fully qualified type"-form fn zip_items<D>(d1: &D, d2: &D) where D: Database, <D as Database>::items(): DoubleEndedIterator, {} ``` One could also use the RTN form as a type, e.g. for local variables. `<>` would be required to use this as part of a fully qualified function calls. **Note that in this context, `D::items()` would instantiate the generic arguments of the method with inference variables not universal variables.** ```rust fn call_items<D: Database>(d: &D) { // Return type of a trait method call: let items: D::items() = d1.items(); // Fully qualified function form (aka UFCS). // You can't write `D::items()::next` because it's ambiguous. // We could use turbofish `D::items::()::next` in addition. <D::items()>::next(&mut items); } fn call_generate_items() { // Return type of a top-level function (note that closures are not in scope). let items: generate_items() = generate_items(0); ... } ``` Finally, although not currently being proposed, this proposal could be extended to cover future use cases: ```rust // Specify the value of an explicit generic fn specify_explicit_generic( d1: impl Database<items_with_key<ItemKey>(): DoubleEndedIterator> // OR d1: impl Database<items_with_key::<ItemKey>(): DoubleEndedIterator> ) {} // Specify the value of an impl Trait argument. fn specify_process_items( d1: impl Database<process_items(ItemProcessor, &str): DoubleEndedIterator> ) {} // Declare a specific method is const using a "const fn bound"? // Lots of question marks here. fn specify_process_items( d1: impl Database<items: const> ) {} ``` ### Strong points * Concise and with relatively few sigils. * At least in what is expected to be its most common form (`T: Database<items(): DoubleEndedIterator>`), this syntax is fairly short. * Rust has a reputation for complexity, so having few sigils (and no unusual ones) is generally less intimidating to readers. * Of course, YMMV: The notion of what is "intimidating" is obviously somewhat subjective. * Able to specify the values for explicit generic parameters. * The syntax extends naturally to turbofish. * Able to specify the values for explicit impl Trait arguments. * The syntax extends naturally to specifying the types used for particular arguments. ### Downsides or concerns that have been raised Some of these points will be addressed by other proposals. * Feels inconsistent with pattern form for tuple structs, where e.g. `Some(..)` means "any number of arguments with any values". * *But* the difference between 0 and 1 ignored pattern can lead to bugs, whereas here the `T::foo(): Send` notation means "prove for all possible values", which is a safe over-approximation (are there counterexamples?). * If we had variadic fns, would be ambiguous between "no arguments" and "unspecified arguments". * We have discussed the idea of variadic functions from time to time. `T::items()` could mean "with all arguments" or "with no arguments". * Feels like a "new concept", even if it ultimately desugars to a bound on an associated type. * the `()` here is "kind of" sugar for `::Output`, but this is a new kind of sugar that doesn't build on the intution of "accessing an associated type of something". * Requires `<>` to use fully qualified functions. * As shown, fully qualified calls like `T::items()::next` would be ambiguous, so `<T::items()>::next` is required. People may not be familiar with this form. We could also support turbofish like `T::items::()::next`. * Potential confusion around the number of arguments: * Note that in type position, `generate_items()` took no arguments and meant "the return type for some set of arguments". * In expression position, `generate_items(0)` took arguments. * This could be confusing to people, it seems like `generate_items()` "ought" to mean that no arguments have been given. * For async functions, people may be surprised that `write(): Send` makes the *future* Send and not the final result. * *But* this is the same as in expression form. * Ambiguity with associated types and no way to fully qualify name. * If we consider `()` as sugar for `::Output`, at least conceptually, an associated type `foo: FnOnce` and a method `foo` would shadow each other, and there would be no way to disambiguate. * This would be compounded if we ever wanted to expose the type of the function itself. * While naming conventions generally distinguish type-space from value-space, we don't generally rely on this at a language level, so it could seem strange to do so here. ## Dot-dot: `T::foo(..)` To address the concerns of inconsistency with pattern form, another option is to require `..` in the parentheses. Functions bounding return types: ```rust // "Associated type bounds"-form (this is the only form currently implemented) fn zip_items<D>(d1: &D, d2: &D) where D: Database<items(..): DoubleEndedIterator>, {} // "Independent type"-form fn zip_items<D>(d1: &D, d2: &D) where D: Database, D::items(..): DoubleEndedIterator, {} // "Fully qualified type"-form fn zip_items<D>(d1: &D, d2: &D) where D: Database, <D as Database>::items(..): DoubleEndedIterator, {} ``` As a type: ```rust fn call_items<D: Database>(d: &D) { // Type of a trait method call: let items: D::items(..) = d1.items(); // Fully qualified function form (aka UFCS). // We could use turbofish `D::items::(..)::next` in addition. <D::items(..)>::next(&mut items); } fn call_generate_items() { // Type of a top-level function (note that closures are not in scope). let items: generate_items(..) = generate_items(0); ... } ``` Finally, although not currently being proposed, this proposal could be extended to cover future use cases: ```rust // Specify the value of an explicit generic fn specify_explicit_generic( d1: impl Database<items_with_key<ItemKey>(..): DoubleEndedIterator> // OR d1: impl Database<items_with_key::<ItemKey>(..): DoubleEndedIterator> ) {} // Specify the value of an impl Trait argument. fn specify_process_items( d1: impl Database<process_items(ItemProcessor, &str): DoubleEndedIterator> ) {} // Declare a specific method is const using a "const fn bound"? // Lots of question marks here. fn specify_process_items( d1: impl Database<items: const> ) {} ``` ### Strong points * Consistent with pattern form for tuple structs, where e.g. `Some(..)` means "any number of arguments with any values". * This form is the "most obvious form". * (Anecdotally, this is where I started initially, and I've found most people I presented RTN to initially asked "why not have `..`"? --nikomatsakis) * Fairly concise. * While not as concise as `()`, this form remains fairly concise. It has more sigils but they are oriented at removing confusion so it will feel more obvious to some readers. * Able to specify the values for explicit generic parameters (as above). * Able to specify the values for explicit impl Trait Arguments (as above). * Unambiguous between "no arguments" and "unspecified arguments" for potential future variadic functions. ### Downsides or concerns that have been raised Some of these points will be addressed by other proposals. * Feels like a "new concept", even if it ultimately desugars to a bound on an associated type (as above). * Requires `<>` to use fully qualified fuctions (as above). * Potential confusion between `generate_items(..)` as a type vs an expression: * Technically `..` is a legal expression; we could use `...` instead, as that is not a legal expression, but (a) that feels inconsistent with patterns; (b) this is not obviously going to be an issue; and (c) it's unclear that `...` would feel less confusing to users, e.g., nikomatsakis forgot that `...` was not a valid expression until this point was raised. * Ambiguity with associated types and no way to fully qualify name. * If we consider `(..)` as sugar for `::Output`, at least conceptually, an associated type `foo: FnOnce` and a method `foo` would shadow each other, and there would be no way to disambiguate. * This would be compounded if we ever wanted to expose the type of the function itself. * While naming conventions generally distinguish type-space from value-space, we don't generally rely on this at a language level, so it could seem strange to do so here. ## Return: `T::foo::return` To address the confusion between `()` in expression position vs type, the syntax `::return` has been proposed. Functions bounding return types: ```rust // "Associated type bounds"-form (this is the only form currently implemented) fn zip_items<D>(d1: &D, d2: &D) where D: Database<items::return: DoubleEndedIterator>, {} // "Independent type"-form fn zip_items<D>(d1: &D, d2: &D) where D: Database, D::items::return: DoubleEndedIterator, {} // "Fully qualified type"-form fn zip_items<D>(d1: &D, d2: &D) where D: Database, <D as Database>::items::return: DoubleEndedIterator, {} ``` As a type: ```rust fn call_items<D: Database>(d: &D) { // Type of a trait method call: let items: D::items::return = d1.items(); // Fully qualified function form (aka UFCS). D::items::return::next(&mut items); } fn call_generate_items() { // Type of a top-level function (note that closures are not in scope). let items: generate_items::return = generate_items(0); ... } ``` Future use cases not currently being proposed: ```rust // Specify the value of an explicit generic fn specify_explicit_generic( d1: impl Database<items_with_key<ItemKey>::return: DoubleEndedIterator> // OR d1: impl Database<items_with_key::<ItemKey>::return: DoubleEndedIterator> ) {} // Specify the value of an impl Trait argument. fn specify_process_items( d1: impl Database<process_items(ItemProcessor, &str)::return: DoubleEndedIterator> // OR d1: impl Database<process_items::(ItemProcessor, &str)::return: DoubleEndedIterator> ) {} // Declare a specific method is const using a "const fn bound"? // Lots of question marks here. fn specify_process_items( d1: impl Database<items: const> ) {} ``` ### Strong points * Can be used in fully qualified function calls without `<>`. * e.g., `D::items::return::next`. * Able to specify the values for explicit generic parameters via `D::items_with_key<ItemKey>::return`. * Able to specify the values for explicit impl Trait Arguments via `D::process_items(ItemProcessor)::return`. * Unambiguous between "no arguments" and "unspecified arguments" for potential future variadic functions. * i.e., presumably `D::items::return` means "all arguments" and `D::items()::return` would mean "no arguments". ### Downsides or concerns that have been raised Some of these points will be addressed by other proposals. * Dense syntax that may be intimidating. * Obviously subjective, but even in its most common form, `D: Database<items::return: Send>` feels fairly "dense" and intimidating. The juxtaposition of `::` and `:`, while not new to Rust, is also unfortunate. * This doesn't feel "dense" to me, but it's certainly more verbose --Josh * Feels like a "new concept", even if it ultimately desugars to a bound on an associated type (as above). * This still feels "new" to me. --nikomatsakis * Feels duplicative with `::Output`. * We've already stabilized `Output` to represent the return type in the `Fn*` traits. If we use `::return` for RTN, it feels like it should work elsewhere also. * This seems to also open the door to things like `::yields`. * Ambiguity with associated types and no way to fully qualify name. * If we consider `::return` as sugar for `::Output`, at least conceptually, an associated type `foo: FnOnce` and a method `foo` would shadow each other, and there would be no way to disambiguate. * This would be compounded if we ever wanted to expose the type of the function itself. * While naming conventions generally distinguish type-space from value-space, we don't generally rely on this at a language level, so it could seem strange to do so here. ## Higher-ranked-projections: `D::items::Output` To address the concern that prior proposals feel like a "new concept" (bounding return value of a function), it [was proposed](https://smallcultfollowing.com/babysteps/blog/2023/06/12/higher-ranked-projections-send-bound-problem-part-4/) that we could write `D::items::Output`, referencing the `Output` associated type defined on the `FnOnce` trait. The idea is to generalize the notation `T::Foo: Bound` where `Foo` is an associated type from the trait `SomeTrait` and `for<'a> T: SomeTrait<'a>` to mean `for<'a> <T as SomeTrait<'a>>::Foo: Bound`. ```rust // "Associated type bounds"-form (this is the only form currently implemented) fn zip_items<D>(d1: &D, d2: &D) where D: Database<items::Output: DoubleEndedIterator>, {} // "Independent type"-form fn zip_items<D>(d1: &D, d2: &D) where D: Database, D::items::Output: DoubleEndedIterator, {} // "Fully qualified type"-form fn zip_items<D>(d1: &D, d2: &D) where D: Database, <D as Database>::items::Output: DoubleEndedIterator, {} ``` As a type: ```rust fn call_items<D: Database>(d: &D) { // Type of a trait method call let items: D::items::Output = d1.items(); // Fully qualified function form (aka UFCS). D::items::Output::next(&mut items); } fn call_generate_items() { // Type of a top-level function (note that closures are not in scope). let items: generate_items::Output = generate_items(0); ... } ``` Future use cases not currently being proposed: ```rust // Specify the value of an explicit generic fn specify_explicit_generic( d1: impl Database<items_with_key<ItemKey>::Output: DoubleEndedIterator> ) {} // Specify the value of an impl Trait argument -- not clear this can be done. // Declare a specific method is const using a "const fn bound"? // Lots of question marks here. fn specify_process_items( d1: impl Database<items: const> ) {} ``` ### Strong points * Builds on existing concepts. * Generalizes to potentially other kinds of higher-ranked scenarios. * Right now `T::Item` is illegal if `T` has a higher-ranked bound; this is a more useful semantics, but is it the right one? ### Downsides or concerns that have been raised * Potential confusion between `Output` on the future and `Output` on the `Fn` type. * To a degree, we've already accepted this by using the same name for the associated types of `FnOnce` and `Future`. * Scope creep: we haven't really evaluated those other higher-ranked scenarios, and it's not clear if this is the behavior you want. Do we really want the same behavior in those scenarios as in function call bounds? * Dense syntax that may be intimidating. * Obviously subjetive, but even in its most common form, `D: Database<items::Output: Send>` feels fairly "dense" and intimidating. The juxtaposition of `::` and `:`, while not new to Rust, is also unfortunate. * No obvious way to scale to values for explicit impl Trait Arguments via `D::process_items(ItemProcessor)::Output`. * Ambiguity with associated types and no way to fully qualify name. * An associated type `foo: FnOnce` and a method `foo` would shadow each other, and there would be no way to disambiguate. * This would be compounded if we ever wanted to expose the type of the function itself. * While naming conventions generally distinguish type-space from value-space, we don't generally rely on this at a language level, so it could seem strange to do so here. ### Migration note Even if we did ultimately adopt this idea, if we adopted any of the prior proposals, we could say that e.g. `()` or `(..)` is just sugar for `::Output`. ## Fn form: `T: Trait<fn method(): Send>`, `fn T::method(): Send` An interesting alternative that arose in discussions about how best to handle `const` bounds was to use the `fn` keyword as a prefix. Bounding return types: ```rust // "Associated type bounds"-form (this is the only form currently implemented) fn zip_items<D>(d1: &D, d2: &D) where D: Database<fn items(): DoubleEndedIterator>, {} // "Independent type"-form fn zip_items<D>(d1: &D, d2: &D) where D: Database, fn D::items(): DoubleEndedIterator, {} // "Fully qualified type"-form fn zip_items<D>(d1: &D, d2: &D) where D: Database, fn <D as Database>::items(): DoubleEndedIterator, {} ``` As the type of local variables: ```rust fn call_items<D: Database>(d: &D) { // Type of a trait method call: let items: fn D::items() = d1.items(); // Fully qualified function form (aka UFCS). // You can't write `D::items()::next` because it's ambiguous. // We could use turbofish `D::items::()::next` in addition. <fn D::items()>::next(&mut items); } fn call_generate_items() { // Type of a top-level function (note that closures are not in scope). let items: fn generate_items() = generate_items(0); // and even? <fn generate_items()>::next(&mut items); } ``` Future use cases: ```rust // Specify the value of an explicit generic fn specify_explicit_generic( d1: impl Database<fn items_with_key<ItemKey>(): DoubleEndedIterator> ) {} // Specify the value of an impl Trait argument. fn specify_process_items( d1: impl Database<fn process_items(ItemProcessor, &str): DoubleEndedIterator> ) {} // Declare a specific method is const using a "const fn bound" fn specify_process_items( d1: impl Database<const fn items> ) {} ``` An interesting twist that this permits is bounding the *return type of an async function*, though then the distinction between `fn request(): Send` (bounds the future) and `async fn request(): Send` (bounds the result of awaiting the future) is quite subtle. ```rust async fn process_request<D>(server: &D) where D: Server< async fn request(): Send >, { let r = server.request().await; send_request(r); } ``` ### Strong points * Mirrors declaration form. * Resolves ambiguity between functions and associated types. * Allows for fully-qualifying the name. * Allows for extending to expose the type of the function itself. * Does not rely on capitalization conventions. * Provides an interesting alternative for `const` bounds (not to overrotate on that). * Able to specify the values for explicit generic parameters. * The syntax extends naturally to turbofish. * Able to specify the values for explicit impl Trait arguments. * The syntax extends naturally to specifying the types used for particular arguments. ### Downsides or concerns that have been raised Some of these points will be addressed by other proposals. * Feels like a "new concept", even if it ultimately desugars to a bound on an associated type. * This obviously feels "very new", in particular not having the bound "look like" `~~~: ~~~~~`. * If we had variadic fns, would be ambiguous between "no arguments" and "unspecified arguments". * We have discussed the idea of variadic functions from time to time. `fn T::items()` could mean "with all arguments" or "with no arguments". * We could of course use `fn T::items(..)` to handle that. * Requires `<>` to use fully qualified fuctions, doesn't compose into type chains very nicely. * The `fn` "prefix" makes composing this into chains kind of visually ambiguous (though perhaps not ambiguous in a formal sense): * e.g., `fn T::items()::next(&mut iter)` as an expression or `fn T::items()::Item` as a type. * For this reason, in the examples above `<>` was require to disambiguate: * `<fn T::items()>::next(&mut iter)` * `<fn T::items()>::Item` ## Evaluation and recommendation **Lang team members are *required* to give their thoughts on preferences.** Other readers are encouraged to do so as well, but please add your names/entries after the lang team member block. Thanks! **Instructions:** * Place your entry in the "big table" and then give a write-up below of what factors influenced your choices. Make sure to explain: * For your top choice(s), what is the key factor that led you there? For things you don't like, what is the key factor that pushed you over the edge? * If you identify considerations that don't appear in the write-up above, please add **FIXME** in your description so I can integrate it. * For anything negative, what don't you like about that option? If something is hard blocked (:x:), what would it take to remove that block (even if you still wouldn't like the design overall)? * Is there a "core tradeoff" for you? ### The options in short | Option | Bound | | --- | --- | | StatusQuo | `D: Database<items(): DoubleEndedIterator>` | | DotDot | `D: Database<items(..): DoubleEndedIterator>` | | Return | `D: Database<items::return: DoubleEndedIterator>` | | Output | `D: Database<items::Output: DoubleEndedIterator>` | | Fn | `D: Database<fn items(): DoubleEndedIterator>` | | FnDotDot | `D: Database<fn items(..): DoubleEndedIterator>` | | FnReturn | `D: Database<fn items::return: DoubleEndedIterator>` | | FnOutput | `D: Database<fn items::Output: DoubleEndedIterator>` | ### Big table Put in * :heavy_check_mark: for your top choice * :green_heart: for things you like but not top choice * :yellow_heart: for things you have significant reservations, even if you like them ("neutral overall") * :red_circle: for things you dislike but are not hard blocking * :x: if you are putting a hard block (if you must) | Person | StatusQuo | DotDot | Return | Output | Fn | FnDotDot | FnReturn | FnOutput | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | *example* | `f()` | `f(..)` | `f::return` | `f::Output` | `fn f()` | `fn f(..)` | `fn f::return` | `fn f::Output` | | nikomatsakis | :yellow_heart: | :heavy_check_mark: | :red_circle: | :yellow_heart: | :yellow_heart: | :green_heart: | tmandry | :heavy_check_mark: | :green_heart: | :red_circle: | :red_circle: | :yellow_heart: | :yellow_heart: | :x: | :x: | | joshtriplett | :green_heart: | :red_circle: | :heavy_check_mark: | :red_circle::red_circle: | :yellow_heart: (1) | :red_circle: | :green_heart: (1) | :red_circle::red_circle: | | pnkfelix | :yellow_heart: | :green_heart: | :yellow_heart: | :yellow_heart: | :heavy_check_mark: | :green_heart: | scottmcm | | TC | :yellow_heart: | :yellow_heart: | :red_circle: | :yellow_heart: | :yellow_heart: | :heavy_check_mark: | :red_circle: | :green_heart: | | errs | :yellow_heart: | :green_heart: | :red_circle: | :yellow_heart: | :yellow_heart: | :heavy_check_mark: | :red_circle: | :yellow_heart: | Nadri | :yellow_heart: | :yellow_heart: | :red_circle: | :yellow_heart: | :green_heart: | :heavy_check_mark: | :yellow_heart: | :yellow_heart: | | eholk | :heavy_check_mark: | :green_heart: | :red_circle: | :red_circle: | :yellow_heart: | :yellow_heart: | :red_circle: | :red_circle: | | (your name here) | ### nikomatsakis Ultimately, my top concern is that the syntax be relatively concise and unintimidating and easy to explain. I find that basically all the options involving parens meet that criteria. I am aware that I expect most uses to be using trait aliases most of the time, but I still think people will encounter the syntax, and I also expect some number of "one-off" bounds that show up internally where a trait alias feels like too much trouble. I am torn between StatusQuo and DotDot. I prefer StatusQuo for the most part -- typing a pair of parens is so pleasant, and it's so compact -- but I cannot get past the "hunch" that we will regret it. Literally every time I explain it to someone, they feel it is inconsistent with pattern syntax. Even if that inconsistency is well justified, I don't relish explaining that for the rest of time. There is also the possibility of variadics, though I admit that weighs less heavily on me (we could for example use `..` in that case) -- but I've also learned not to dismiss these kinds of forward-looking concerns, since often an inconsistency that seems small now feels larger when we start driving the future design forward. (One other thought: `(..)` feels a bit more surprising if we never allow specifying the types of individual parameters; it strongly suggests you can put things in there. I'm not sure if we really want to go that way or not.) I am surprisingly intrigued by the Fn and FnDotDot options, though they seem kind of "out there". My biggest hesitation is that I have to think kind of hard to figure out where the `fn` keyword should go. It kind of reminds me of C's syntax for function pointer types: even though it's fairly logical, it's always a puzzle to write `typedef void (*foo)(u32)` or whatever the heck it is (did I get that right?). **I would be interested in the idea of some kind of experiment of trying to write a module or two using it to see how it feels.** I like the Output option as well, but for very different reasons. It does not feel "nice" to use, but it feels like a logical-ish extension of the current system. I am worried about how docs and other things will look when people encounter it in the wild, though, I think it's going to be dense and hard to visually parse. Overall I'd be interested in considering this extension in the future. That said, I would feel fine about `(..)` being a shorthand syntax for it. I am not sure how much to worry about the confusion between `Output` on the `Future` trait and `Output` on the `Fn` traits -- it might be an issue, but that's already a kind of confusion (i.e., is this bounding the result of async fn or what) and I'm not sure how much worse it'll be. I have strong reservations about `::return`. Unlike `::Output`, it doesn't feel like an extension of what we have, it feels like something new -- it's a keyword and it refers to the return type of a method, but it is verbose and takes me time to read and think about. I would not hard block it if others were aligned around it of course. I am not too worried either way about specifying the values for generic parameters or the types of arguments, though I do expect it'll come up and I like having some options. ### tmandry My current top choice is `f()` because of this point: > But the difference between 0 and 1 ignored pattern can lead to bugs, whereas here the T::foo(): Send notation means "prove for all possible values", which is a safe over-approximation (are there counterexamples?). But I still can see why we would prefer `f(..)` and go back and forth on this point myself. Looking at the code examples makes me dislike `::return` and `::Output` more, from the standpoint of sheer verbosity. I think we should strive to minimize the verbosity of writing Rust code where possible. I also dislike `::return` because of how special (and out of place) it is, and `::Output` because of its ambiguity with `Future`. If I could go back in time and rename `Future`'s associated type I probably would, and then I would be neutral on `::Output`. My concerns with `fn f()` are that it looks too much like function pointer syntax in type position. The line between "this represents a function" and "this represents the return type of a function" has become too thin. The only problem with this is that we might want a way to bound by `const` in the future. Maybe something like this can work: ```rust fn specify_process_items( d1: impl Database<process_items: const fn(ItemProcessor, &str)> ) {} ``` I don't understand why someone would combine the `fn` proposals with a projection, hence my hard blocking designation, though I'm willing to reconsider. It seems like it's mixing two concerns together. Perhaps we should have this as a means of namespace disambiguation, but I would not support it as the primary syntax for users. ### joshtriplett Aside: in the future, can we please not use "status quo" for "current proposal"? The status quo is that we don't have a syntax for this. > If we use `::return` for RTN, it feels like it should work elsewhere also. What are the other places it should work? I expect that my answer would be "yes, it should work anywhere it makes sense" (any time you have a function type); for instance, if you have `F: FnOnce(u32) -> u64`, `F::return` should work (and be `u64`). In general, I feel like `::return` is extensible and orthogonal in a way I find elegant. However, I can already see people's feelings on it from the table, and while I'd like to *understand* that better, I'm not going to fight for it from a perspective of it being my top choice and most other people's last choice. I do think we should provide a way to get the type of the method itself, as well. I think this is another factor to consider here: extensibility. `method::return` goes naturally with `method::arg` or similar, as well as with `method::fn` to get the method type itself if you need to do that explicitly. (Though if you're already in type space it may make sense for `method` to juts *be* the function type.) I would like to better understand what it means for `::return` to be "dense". It is definitely noticeably longer than `()`, and that *is* a downside. It doesn't seem *semantically* more complex, though, insofar as the concepts we're referencing (the name of the return type). I find the argument that `::Output` has the potential for confusion very compelling, to the point that I think that outweighs its advantages. I think `::Output` is *more* strongly associated with Future than Fn traits, because you have to write `Future<Output=...>` but we have syntactic sugar for `Fn(...) -> ...`. While I don't love having to use the turbofish syntax, I think it does fully address the concern I had about being able to specify argument types where needed. (And I don't think we should tie any of this to a new syntax for `impl Trait` arguments.) So, I'm going to drop that concern: I do agree that it's possible for most of these syntaxes, and not unique to `::return`. For the same reason, I don't feel like `..` is a sufficient win over `()` to be worth what *feels* like syntactic salt. Regarding the `()` syntax, I feel like it's the least bad option here, at this point. My only forward-compatibility concern is whether this will end up taking away options when in the future we have type-level functions (e.g. functions that return types and can be evaluated at compile time). Is there a way we can hedge about that. Note (1) in the table above: If `fn` is an optional disambiguator I would be :green_heart:, if there are cases where we *need* disambiguation. But as far as I can tell, we would only need disambiguation in the case of capitalized method names or lowercase type names, and I'm not sure that's worth a whole disambiguator syntax. In any case, I would object to a syntax that *mandates* it. ### pnkfelix I decided my top choice is `fn foo()`. The presence of the `fn` keyword in the `fn foo(..)` and `fn foo()` options is a good signal to the user that something interesting is going on. And (as noted in doc) it also provides a natural extension point for e.g. `async fn`. So I'm pretty sure my green checkmark is going to go with one of those two options. I don't mind `fn foo(..)` at all, but if we can get away with `fn foo()`, then I think it would be nice to avoid **so** much ceremony. (I.e. I can imagine people being annoyed by requiring both the `fn` and the `..`. (Unless we followed up quickly with features that justified the `..`.) So that's why I put my :heavy_check_mark: with `fn foo()`. I think `foo()` on its own is too subtle. `foo(..)` restores the balance a bit there, so that I don't regard it as *too* subtle. ~~As for `foo::return` and `foo::Output`, I have decided I dislike them equally. I think the `foo::Output` option is potentially the most "natural" choice from the viewpoint of what pre-existing features exist in the language+stdlib...~~ after reflection, I decided I don't dislike these *that* much, and have put them both on the same :yellow_heart: footing as `foo()` for my row. ### scottmcm ### TC I agree with all points of nikomatsakis' analysis. On the points which he expressed feeling torn, I feel torn similarly. (Consider everything he said above incorporated here by reference.) The one concern I would perhaps emphasize more strongly is that of ambiguity between type and value space. I.e.: ```rust trait Foo { type foo: FnOnce(); fn foo(); } fn test<F: Foo<foo::Output: Sized>>() {} // ^^^ // Does this refer to the function or to the associated type? ``` (Rust treats types and values (including functions) as occupying separate namespaces.) While we could say that the `()`, `(..)`, or `::return` forms would not, strictly speaking, be ambiguous if they only worked with functions (and not types), this would break the mental model that these forms are "just sugar" for `::Output`. To the degree that people would *think* of these as sugar for `::Output`, these would carry the same risk of ambiguity. And this ambiguity could be a problem if we ever wanted to more literally make e.g. `(..)` simple sugar for `::Output` or we wanted to expose the type of the function itself. In terms of decisions we could make on the syntax about which we might later have regret, this seems a plausible candidate. Here's a proposed **design axiom**: > If we have two namespaces when *defining* types and values (including functions), then when *referencing* these, we should have distinct syntax for each namespace. Everywhere that our "most fully-qualified form" is not actually fully-qualified *enough* seems like a mistake in retrospect (c.f. having no way to refer to shadowed inherent methods on a trait object, which is [question 10](https://dtolnay.github.io/rust-quiz/10) on dtolnay's quiz). Once we stabilize ATPIT (associated type position impl Trait), it will become more common to have associated types that implement the `Fn*` traits (trivially, because it will go from being impossible to being possible). So this ambiguity could begin to matter in new ways. While we could hope that naming conventions might save us here, there are reasonable use cases in macros for violating these convensions. Similarly, one of the design axioms proposed in the recent RTN meeting was: > **The Rust language shouldn't make opinionated choices about names etc.** We'd prefer to avoid encoding conventions -- e.g., converting method names from snake case to camel-case -- as part of the language rules themselves. Those kind of conventions are great in API definitions and procedural macros. For this reason (and to not add any more questions to dtolnay's quiz), my preference is one of the `Trait<fn foo(..): Bound>`-style forms (or any alternate proposal that similarly distinguishes referencing these two namespaces). Ergonomically, I prefer either `fn foo(..)` or `fn foo()`. For the sake of *consistency*, I believe we should *also* support `fn foo::Output` and treat the `(..)` or `()` forms as sugar for this. However, overall, I'm in favor of RTN in any reasonable form, and I'll unreservedly support whatever consensus we align around. ### Nadri My two cents: `fn foo()` scans well, expecially in the middle of a complex type. It's unambiguous that we're talking about a function, so the meaning can be inferred from syntax. That feels crucial to me. I see the further possibilities like `async fn`/`const fn` as a sign that this new concept has weight to it. I wasn't feeling that with `foo()` syntax. `(..)` has the benefit of looking like a pattern, so would expect extensions like `fn foo(_, SomeType, ..)`. `fn foo()` feels like it's comitting to never allowing arguments later. --- # Design meeting Date: 2024-03-04 People: TC, nikomatsakis, tmandry, pnkfelix, Josh, CE, eholk, Nadri Minutes/driver: TC (Please write down your analysis above (and read the analyses of others) before filling in topics below.) ## position of `fn` keyword in Fn form pnkfelix: The downsides section for Fn form references a few examples written like `<fn T::items()>::Item`. I am just curious: Is there an, um, "obvious" reason that this would not instead be written `<T::fn items()>::Item`? (I'm not claiming that this change in position would do anything to address the listed downside regarding `<~~~>` and such; I was just wondering about what seemed "natural" when I was reading the text, in terms of whether I would prefer to have the `fn` near the function name, or as a much earlier prefix before we write the path at all.) nikomatsakis: Not really, I wanted to mirror the declaration syntax, and `::fn` "feels" dense to me (e.g., `Foo: Database<items::fn: Send>`). Josh: I also feel like combining a syntax involving a space with a syntax involving `::` feels like it creates a "grouping" that doesn't exist: `T::fn items()` looks like `T::fn` followed by `items()` to me. Josh: That said, I personally like the idea of `items::fn`, for the same reason I liked `::return`, but I think that would mean the function type, not the return type. (The meeting ended here.)