---
# System prepended metadata

title: 'Design meeting 2024-02-16: Josh''s concerns on RTN'
tags: [T-lang, minutes, design-meeting]

---

---
title: "Design meeting 2024-02-16: Josh's concerns on RTN"
tags: ["T-lang", "design-meeting", "minutes"]
date: 2024-02-16
discussion: https://rust-lang.zulipchat.com/#narrow/stream/410673-t-lang.2Fmeetings/topic/RTN.20meeting.202024-02-16
url: https://hackmd.io/Yne6UYvkRTiEv5OZspCfxw
---

# RTN Analysis and Concerns - Josh

(Original document is [here](https://hackmd.io/nbMtmAA2SsS5EbP3Omhwsg).)

## Summary

My concerns about RTN fall in two categories:
- Community impact and interface fragility/instability caused by the availability and use of RTN.
- The syntax of RTN not mirroring expression syntax, and being ambiguous in expression contexts, which closes off multiple future possibilities.

Based on further consideration, I no longer want to put forth an alternative to RTN.

Instead, I've proposed a set of mitigating mechanisms to address the former concern, and proposed specific requirements for potential RTN syntaxes to address the latter concern.

## Acknowledgement of the problem RTN solves

I very much appreciate the premise and value of being able to name the return value of an arbitrary trait method. I don't have any issue with that. I'm enthusiastic about more introspection mechanisms in general.

I like the idea of discouraging people from naming associated types when those types don't otherwise need a name, solely so that downstream users can write bounds on those associated types. I'm in favor of reducing the amount of "in case downstreams need it" boilerplate that library authors have to write, and that downstreams will push them to write if they don't.

And I'm a big fan of steps we can take towards "permissionless innovation", where we give people the capabilities needed to let them do innovative things that weren't anticipated by (for instance) their dependencies or other crates in the ecosystem.

## Potential community impact of RTN availability

Sometimes, providing a feature can steer the community in a particular direction, whether we intend that or not. There are some features that we've empirically observed people reaching for when they shouldn't; for instance, people who come from a language with global mutable variables reach for `static mut` when there are much better solutions. What we provide in Rust, how we present it, and how readily at hand it feels in different contexts, influences the ways people use Rust and the ways the Rust ecosystem evolves.

Consider the history of C++ generics. Historically (and still in widespread current practice), C++ generics effectively use compile-time [duck-typing](https://en.wikipedia.org/wiki/Duck_typing): your requirements on a C++ generic type are precisely what you do with it, without having to declare those requirements. Whether you can instantiate a template function depends on the body of the function, which makes abstraction and encapsulation difficult. Modern C++ introduced "concepts", which provide benefits similar to our traits, though much code still doesn't use them. Rust avoided that from the outset, by using only traits and not duck-typed generics.

I think RTN has a danger of devolving into "do duck-typing and then add RTN bounds until it compiles", rather than encouraging better interface design such as defining a trait or trait alias. We're talking about RTN in the context of Send bounds, but I think some developers learning Rust and trying to get their code to compile may end up writing bounds like `T::method(): PartialOrd + Debug`, which has many problems and seems like the wrong tool for the job. Most of the time, users should define a trait or a trait alias, and use that. Or, sometimes, they should be using a concrete type rather than a generic.

Using RTN in a function or data structure bound, rather than defining an interface, seems likely to make interfaces more fragile and less encapsulated, similar to C++ duck-typed generics. Once you've put RTN in the bounds of a public function, for instance, adding or removing a bound is likely to be a breaking change.

Proposed mitigating steps:
- **Ensure that we've defined and stabilized a trait alias mechanism before (or concurrently with) stabilizing RTN.** If we don't have a trait alias mechanism, people will have no option other than to inline their RTN usage on callers; having a trait alias mechanism lets us encourage people to use RTN primarily to define trait aliases.
- **Avoid adding any compiler suggestions that steer people towards RTN directly in bounds.**
- **Provide a lint against using RTN for bounds outside of a trait alias.** Make the lint warn-by-default for private uses, and deny-by-default for public interfaces, with suggestions about what to do instead, so that people are unlikely to *accidentally* create a fragile bespoke interface by using RTN in a function's or data structure's bounds. We can always change/relax this lint if people find other creative uses for RTN that we *don't* want to discourage.
- **Use trait aliases in our messaging and documentation about RTN.** Given the above, we'd need to use examples consistent with the usage we want to see.

Mitigations considered and rejected:
- Initially only permit RTN syntax in the same crate as the trait definition? This would sacrifice "permissionless innovation", but would encourage the definition and sharing of aliases like `ServiceSend`. I don't think this tradeoff is worth the cost, and it runs counter to the principles that (for instance) motivate relaxing the orphan rule.

## Syntax issue with the proposed RTN syntax

**I'd like to avoid bikeshedding syntax in this meeting.** Instead, I've proposed concrete requirements to apply to potential syntaxes, and I'd like to evaluate the requirement and then have *asynchronous* discussions about possible syntaxes that meet that requirement.

Writing `T::func(): Send` looks and feels wrong, in that the type syntax doesn't feel like it mirrors the expression syntax, not least of which because it uses nullary `()` no matter what arguments the function takes.

In addition to looking and feeling wrong, and throwing off people's mental parsers, this syntax also closes off some future possibilities that we may want:

- Uses of RTN in contexts that could be either a type or an expression. For instance, consider the case where `Type` is the return type of a function and you want to write `Type::CONST` or `Type::method(...)`. The proposed RTN syntax would be ambiguous with a function call.
- Uses of RTN that might depend on the parameter types of the function, or hypothetically in the future on the arity of the function (e.g. if we ever add safe varargs to Rust).

It's also less extensible to other things that we might want to introspect about a function other than its return type, such as its argument types or arity. For instance (straw syntax for illustration purposes), consider if we had `method::return` and `method::args::1` and `method::ARITY` and similar.

I would propose that we pick *some* syntax that has all of the following properties:
- Equally valid and unambiguous either in expression context *or* in type context
- Not ambiguous between "nullary" and "unspecified arguments"
- Ideally leaves room for future properties of a function

I don't want to bikeshed the actual syntax used; I'd just like a syntax that has these properties.

---

# Discussion

## Attendance

- People: TC, Josh, nikomatsakis, tmandry, Urgau, CE, fmease, Yosh

## Meeting roles

- Minutes, driver: TC

## Not duck typing

> I think RTN has a danger of devolving into “do duck-typing and then add RTN bounds until it compiles”, rather than encouraging better interface design such as defining a trait or trait alias.

tmandry: RTN doesn't allow duck typing because the methods you bound must already belong to a trait. If you have a bound like `T::method(): Foo`, then you must also have a bound like `T: Bar`, where `Bar` is a trait containing `method`.

We might choose to allow bounding inherent methods, but that would only work with concrete types, not any `T`.

JT: The duck typing I'm referring to is the ad-hoc addition of bounds to the return values. Yes, you have to start out with a function or trait method, but the bounds on the return value could be defined based on precisely what you need from the return value. Send seems fine; PartialOrd + Add + Debug seems more like "write down bounds until it compiles".

tmandry: How is RTN different from associated types in that regard? People can already choose to turn their code into generics spaghetti soup, but we offer tools like traits and blanket impls (and in the future, trait aliases) to help them clean it up.

JT: What I'm talking about here is when you're externaling the things in the body into the bounds.

TC: You're talking about this, e.g.?

```rust
fn frob<W, X, Y, Z>(w: W, x: X) -> Z
where
    W: Add<X, Output = Y> + Copy + Debug + Send + Into<u64> + 'static,
    X: Mul<Y, Output = Y> + Copy,
    Y: Div<W, Output = Z>,
{
    dbg!(w);
    thread::spawn(move || sleep(Duration::from_millis(w.into())));
    x * (w + x) / w
}
```

JT: That's certainly an example, yes. Frankly, if we had a reliable way to catch and lint against *that* I'd encourage it.

NM: This is a possibility today.  You could do the same with e.g. associated types.  The basic mitigation that our language has against this is that we have traits that are groups of related functionality.  You add a bound to get access to a variety of methods.  That isn't changing with RTN.

NM: Imagine I have an async trait with two methods.  So someone could put the bound on one method, but then someone could later want to call the other method.  So that's a SemVer commitment.

JT: You mentioned that's a feature we want.  I haven't seen why that is. What is the use case for having a trait with multiple methods, having an impl that makes some but not all of them return something meeting `Send`, and having a caller that only needs those to be `Send`?

NM: It does come up a lot.  Most examples for me take the form of some code that I want to run in multiple situations.  E.g.:

```rust
trait DataStore {
    fn keys(&self) -> impl Iterator<Item = String>;
    fn values(&self) -> impl Iterator<Item = String>;
}

impl DataStore for TestDataStore {
    fn keys(&self) -> vec::IntoIter<String> {
        vec![...].into_iter()
    }
    fn values(&self) -> impl Iterator<Item = String>;
    
}

impl DataStore for OtherDataStore {
    fn keys(&self) -> hash_map::Keys<String> {
        // keys are in a HashMap
        todo!()
    }
    fn values(&self) -> impl Iterator<Item = String> {
        
    }
}

fn do_stuff<T: DataStore>() {
    for key in t.keys().rev() {
        
    }
}

// I don't have a problem here exposing this bit of
// of my implementation.
fn do_stuff<T: DataStore>()
where
    T::keys(): DoubleEndedIterator,
{
    for key in t.keys().rev() {
        
    }
    
    for value in t.values() {
        
    }
}

```

Similar example with `Send`:

* Sometimes I have methods that e.g. use `Rc` and it so happens that, in the parallel paths, I don't need to use those methods.
    * Without RTN I have to anticipate in advance the set of methods to group out.

JT: To me, this is an argument that a trait alias should be used instead.

NM: There isn't a canonical set, and it's useful to be able to define arbitrary sets separate from the trait definition.

JT: It seems like you have multiple functions that want the same thing.

NM: I want aliases here for the same reasons I want aliases elsewhere, to reduce verbosity.  This doesn't seem to me to relate to SemVer.

Josh: Is this in public API, or in local interfaces within a codebase?

NM: There could be a trait that defines a few methods... if you're asking, "is this something my crate exports?", probably not.

NM: A useful angle is to look at this from a SemVer point of view.  Suppose there is a public trait.  What are the situations where you would get errors we would not expect?  E.g. when adding a new method.

JT: I'm asking about public APIs here because I'm less worried about this when used privately since it doesn't involve SemVer concerns.  We could reduce the lints I proposed in this way.

NM: Imagine you have a public trait in crate A and a consuming function in crate B...what happens when you do:

* Add a method to the trait (with a provided definition, else that's a semver breaking change).
* Modify the function to call add'l methods from the trait.

```rust
trait Foo {
    async fn foo();
    
    // This is added later.
    async fn new_method() {
        let x = Rc::new(22);
        foo().await;
        drop(x);
    }
}
```

NM: If someone has bounded on `Foo`, and a new method is added, you want those people to be able to use those new methods.

Now...

* If I were using RTN, I don't care that `new_method` is not `Send`, which is good, beacuse I don't break
* but if `Foo` 

NM: If traits define an alias, it has SemVer implications.  It's not just a shortcut.

NM: If every trait had an alias, and people didn't know it existed, and you were using alias, and the trait author later adds a method, they could break you.

NM: Put another way, if you had a design that said `Send Foo` and it meant "the trait `Foo` where all async fns are `Send`". Now if a new async fn is added with a provided definition that is not `Send`, that's a breaking change to any users of `Send Foo`. (Same thing we say about `?const` traits.)

Yosh: We have a trait and people using it, when new methods are added, we want people to be able to add a non-`Send` method, is that correct?

NM: That's a version of it.  There are two motivating use cases.  One is, for the person who defines the trait, defining a set of methods that will have a set of bounds that will be extended together.  The other is having a function that can be specific about what it needs so the function can be reused as best possible.

NM: When you're defining a generic function, there's a tradeoff between how often you can use a function and what changes you can make to the implementation (without knowing what types you are being invoked with). Some patterns are very limiting to the changes you can make in the impl (i.e., limit you from calling more methods than you are already using). Other patterns are very limiting to the places you can use it.

JT: There's value in higher level groupings of the things that you might want.  If you can give a name to the thing, it makes it more likely you can evolve it in the future.

## Mitigating steps

JT: What mitigating steps do people disagree with?

1. **Ensure that we've defined and stabilized a trait alias mechanism before (or concurrently with) stabilizing RTN.** If we don't have a trait alias mechanism, people will have no option other than to inline their RTN usage on callers; having a trait alias mechanism lets us encourage people to use RTN primarily to define trait aliases.
2. **Avoid adding any compiler suggestions that steer people towards RTN directly in bounds.**
3. **Provide a lint against using RTN for bounds outside of a trait alias.** Make the lint warn-by-default for private uses, and deny-by-default for public interfaces, with suggestions about what to do instead, so that people are unlikely to *accidentally* create a fragile bespoke interface by using RTN in a function's or data structure's bounds. We can always change/relax this lint if people find other creative uses for RTN that we *don't* want to discourage.
4. **Use trait aliases in our messaging and documentation about RTN.** Given the above, we'd need to use examples consistent with the usage we want to see.

JT: I'd like it if there's a higher level thing that can build on RTN.

tmandry: The `trait-variant` macro does allow this, including that one can implement one of the traits and we provide blanket impls for the others.

JT: What's the situation on trait aliases?

NM: I don't want to block on it, but it may not be far away if we focused on it.

CE: There are no serious concerns on the implementation.  It mostly comes down to what semantics we want.  There's nothing in the trait solver that's problematic for trait aliases right now.

CE: To the compiler, RTN bounds are just regular associated type bounds.

tmandry: What about implementable trait aliases?

CE: No concerns, as long as there are reasonable restrictions such as that only one trait applies there.  It's similar to `dyn` in that respect.

JT: We probably don't even need implementable trait aliases.  Just bare trait aliases is probably enough.

tmandry: You can simulate trait aliases today.

JT: Less friction is important here.

fmease: If the trait aliases have to be defined upstream, doesn't that still have the problem of adding new methods?

fmease: So there's no way to express that non-`Send` methods could be added later to the trait?

NM: You'd have to annotate it.

---

JT: It sounds like we have a trait alias mechanism that is not too far off.  Could we commit to shipping that concurrently with RTN?

NM: I could live with it.

tmandry: It would be better to ship them together.  But if we're waiting 3-6 months for it, then I would reconsider.

## Alternate proposal from Niko

nikomatsakis: can we edit to arrive at a consensus...?

- **Ensure we have a mechanism for defining "send" or other aliases (via a proc macro).**
    - Blocking need is: impl the base + get the bounds.
    - *Strongly preferred* version is to have stabilized trait aliases.
        - Bonus points if they are implementable.
    - Does it have to be in libstd or can it be a rust-lang crate?
- **Define lints that encourage people to use trait aliases if they exist.**
    - (Q: how do we know that they exist? can lean on convention here)
- **Use trait aliases in our messaging and documentation about RTN.** Given the above, we'd need to use examples consistent with the usage we want to see.
    - Explain trait aliases first.
    - This makes sense and is easy.
- **Warn by default lint against using RTN in bounds on a reachable public function: suggest defining a trait alias instead.**
    - Josh: reachable public, yes - "if it's subject to semver"
    - Niko: just to be clear, modifying this alias will still be a breaking change -- but it will encourage people to think about the right "closure" of methods they may call and make it easy to refer to that.
    - tmandry: If you own the trait, you can add a provided method simultaneously too (yes! --niko).

Josh: I care more about the public interface case: could scope down to only having rustc lint on having RTN bounds directly in public interfaces. Then, if you really want ad-hoc RTN bounds you can allow the lint, but there's *steering* towards designing an interface via trait aliases.

*Consensus on semantic concerns*: We'll try to ship trait aliases (but not necessarily implementable trait aliases) concurrently with RTN (but we'll reevaluate if those are taking too long), and we'll ship RTN with a warn-by-default lint against the direct use of RTN in the bounds of a reachable crate-level public interface (e.g. function, method, type (or type alias), trait, etc.) except for a trait alias.

*Outcome on syntax concerns*: We did not discuss the syntax concerns.  Those are still blocking, and we'll try to resolve those asynchronously.  We'll check in at the next triage meeting how that is going.

(The meeting ended here.)

---

## Expression contexts?

> contexts that could be either a type or an expression

tmandry: I don't know of such contexts that exist in Rust today. What are some examples of contexts where we might want to allow this? Supplying expressions as const parameters without surrounding with curly braces?

JT: I'm talking about expression contexts in which you can start out writing a type before you write `::`, for instance.

tmandry: I see, so UFCS for instance. It is ambiguous in those contexts, though I think we could fix that with a turbofish-like form. We would need a new name. How about starship?

`Foo::method::()::poll(fut, cx)`

Of course, maybe it's better to use a type alias in these cases when you can.

Josh: Can we please not create a *new* syntax like turbofish, when we potentially have an alternative that allows using the same syntax for type and expression? :)

tmandry: Niko mentioned that we can use

`<Foo::method()>::poll(fut, cx)`

## Projection

> consider if we had method::return and method::args::1 and method::ARITY and similar

tmandry: We could project out like this if we had RTN, the user would omit parentheses. The question is whether we would want projections that are dependent on argument types. Supplying argument types is a natural extension of the RTN syntax, but RTN itself would not allow introspection of anything other than the return type.

JT: These are future possibilities, and ones that seem more compatible with using a different syntax that doesn't use the empty parentheses.

tmandry: What do you mean by "more compatible"?

One concrete issue I could see with RTN is if we wanted to bound a method as const, dependent on the argument type. I was thinking we could write const bounds like this:

`where Type::method: const`

but if we need to supply argument types (think higher order functions), that would not work. Maybe could instead write

`where const Type::method(F)`

Josh: By "more compatible" I mean that I think `method(): Bound` is less extensible to things like argument types and arity, whereas `method::return` has obvious parallels in `method::ARITY` and `method::args::1` and similar. That's one example syntax, but one that admits other variations in a way that `()` doesn't.

And the example you give of depending on argument types is exactly my concern about the empty parens; if we need to depend on argument types, then we should only use `()` for a function of zero arguments.

## niko's general take

nikomatsakis: Generally agree with the concern as described that RTN presented the possibility of people authoring "overly specific" sets of bounds. This is both its power and its risk -- I think a lot of the use-cases I can think of for having different sets of bounds on functions come down to trying to extract out some common code for particular scenarios where you know bounds will be met (e.g., I happen to know that in the various cases where I invoke this function, some particular trait method returns a double-ended iterator, and I'd like to take advantage of that). i.e., sometimes it's good to be able to carve out code. But it does create the risk that people will 

Tyler and I had some discussion about precisely this and we realized though that the situation was a bit more complex. It seems like the rules we want are...

* Traits should be encouraged to define a send-alias (and other potential aliases).
* blah blah

I'll write more later, going to finish reading.

## niko not a big fan of linting against using RTN

nikomatsakis: The lints against using RTN seem overly broad to me. I was thinking of lints that push people towards defining aliases, something like

* if you add an RTN to every method in a trait bound, and the trait is local, recommend defining a send alias
* if you add an RTN to every method in a trait bound -- or maybe some methods? -- and the trait is remote, and an alias exists, recommend using the send alias instead

but linting against using RTN at all seems way too broad to me.

Josh: Nit: "every method" may not be the case, since Send might only make sense for the async methods, for instance. But I understand what you're proposing in general, that we lint against cases where we think it likely the 

## We crossed this bridge?

TC: The desire, it seems, is to avoid code like this:

```rust
fn frob<W, X, Y, Z>(w: W, x: X) -> Z
where
    W: Add<X, Output = Y> + Copy + Debug + Send + Into<u64> + 'static,
    X: Mul<Y, Output = Y> + Copy,
    Y: Div<W, Output = Z>,
{
    dbg!(w);
    thread::spawn(move || sleep(Duration::from_millis(w.into())));
    x * (w + x) / w
}
```

...but this doesn't need RTN, unnameable types, or even traits.  It seems like we crossed this bridge long ago.

yosh: I thought the issues with RTN were less about that, and more about cases like these?:

```rust
/// Forward an `impl AsyncRead` which guarantees
/// all of its methods return `+ Send`.
///
/// Notably, this is not forward-compatible 
/// if we introduce any new methods because
/// we have no "for all methods" bound.
fn wrapper<R>(reader: R) -> R
where
    R: AsyncRead<
        read(): Send,
        read_vectored(): Send,
        is_read_vectored(): Send,
        read_to_end(): Send,
        read_to_string(): Send,
        read_exact(): Send,
        read_buf(): Send,
        read_buf_exact(): Send,
        by_ref(): Send,
        bytes(): Send,
        chain(): Send,
        take(): Send,
    >,
{ reader }
```

## Thoughts on syntax

nikomatsakis: the doc outlines three requirements for syntax...

> I would propose that we pick *some* syntax that has all of the following properties:
> - Equally valid and unambiguous either in expression context *or* in type context
> - Not ambiguous between "nullary" and "unspecified arguments"
> - Ideally leaves room for future properties of a function

...I believe the first one is going to be the hardest to fulfill. In fact, our existing types don't meet it (hence turbofish). My personal preference is to do `<T>::foo()` when invoking methods on a type that isn't just a plain identifier or path. I wonder if that suffices? I'm sympathetic to the last two, which I note would I think be fulfilled by just `T::foo(..)` -- it seems verbose, but not excessively so.

Josh: Many of our types do meet it, though. Including associated types, and *almost* generic types (modulo having to add `::`, but they at least look closely similar).

## Separate feature for separate axis of concern

TC: It seems what is desired here is a separate feature that would allow an upstream to prevent downstreams from setting bounds on return values.  Not saying that we want that feature, just that there's a separate axis here and mixing this with unnamebale types seems perhaps to be less clear.

## Trait Transformers

yosh: Where do we see [trait transformers](https://smallcultfollowing.com/babysteps/blog/2023/03/03/trait-transformers-send-bounds-part-3/) fall in this space? I haven't been a part of the last few T-lang conversations on RTN, so I'm not sure what the latest thinking about it is. But the way I understood it, it would present a credible solution to some of the RTN issues inherent to RTN?

## alternative mitigation proposal

nikomatsakis: can we edit to arrive at a consensus...?

- **Ensure we have a mechanism for defining "send" or other aliases (via a proc macro).**
    - *Preferred* version is to have stabilized trait aliases and in particular implementable ones.
    - Does it have to be in libstd or can it be a rust-lang crate?
- **Define lints that encourage people to use trait aliases if they exist**
    - (Q: how do we know that they exist?)

The final point is encouraging people to define trait aliases themselves. I'm in favor, but more narrowly. For example:

- **Avoid adding any compiler suggestions that steer people towards RTN directly in bounds.**
    - It's easy to imagine a tailored bound suggestion. It's a bit harder to imagine a suggestion to add a trait alias.
- **Use trait aliases in our messaging and documentation about RTN.** Given the above, we'd need to use examples consistent with the usage we want to see.
    - This makes sense and is easy.
- **Provide a lint against using RTN for bounds outside of a trait alias.** Make the lint warn-by-default for private uses, and deny-by-default for public interfaces, with suggestions about what to do instead, so that people are unlikely to *accidentally* create a fragile bespoke interface by using RTN in a function's or data structure's bounds. We can always change/relax this lint if people find other creative uses for RTN that we *don't* want to discourage.
    - Seems too strong.
