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)[1] implemented, along with some of its strengths and weaknesses. This will also introduce a sample program that we can port to future syntaxes.

Given the following trait and top-level function

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:

// "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.

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:

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

// "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:

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:

// 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 © 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:

// "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:

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:

// 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 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.

// "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:

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:

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

// "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:

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:

// 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.

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 (
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
      ), 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

  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    for your top choice
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    for things you like but not top choice
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    for things you have significant reservations, even if you like them ("neutral overall")
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    for things you dislike but are not hard blocking
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    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
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
tmandry
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
joshtriplett
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
(1)
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
(1)
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
pnkfelix
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
scottmcm
TC
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
errs
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Nadri
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
eholk
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
(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:

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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
, 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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
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.:

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


  1. Only the "associated type bounds" form is currently implemented. ↩︎