owned this note
owned this note
Published
Linked with GitHub
---
title: "Design meeting 2024-01-24: Solving the Send bound problem in 2024"
tags: ["T-lang", "design-meeting", "minutes"]
date: 2024-01-24
discussion: https://rust-lang.zulipchat.com/#narrow/stream/410673-t-lang.2Fmeetings/topic/Design.20meeting.202024-01-24
url: https://hackmd.io/rmN25qziSHKT4kv-ZC8QPw
---
# 2024-01-24: Solving the send bound problem in 2024
## Premise
Now that we have stabilized async fn in trait, we are almost at the point where we can begin to bootstrap an interoperable Async Rust ecosystem, but one hurdle remains: the [send bound problem][sbp].
[sbp]: https://smallcultfollowing.com/babysteps/blog/2023/02/01/async-trait-send-bounds-part-1-intro/
## The pattern we want to support: Generic Service
The primary goal is to permit highly generic traits that include async functions. The prototypical example is Tower's `Service` trait. The [current definition](https://docs.rs/tower/latest/tower/trait.Service.html) of Service involves a manual `poll_ready` function. The goal would be to enable `Service` to be written something like as follows:
```rust
trait Service<Request> {
type Response;
// Invoke the service.
async fn call(&self, req: Request) -> Self::Response;
}
```
### What breaks today?
The `call` method returns a future. In work-stealing contexts, like the default Tokio setup, the future must be `Send`, but in other contexts (e.g., embedded or single-threaded-executor setups), any future will do. To be most compatible with how Rust works, we want to be able to define a single `Service` trait, but have some capability to say "give me a type `T` that implements `Service` but where `call` returns a future that is `Send`".
Consider this function that takes some service `S` and spawns a task to execute it:
```rust
async fn spawn_call<S>(service: S) -> S::Response
where
S: Service<()> + 'static
{
tokio::spawn(async move {
service.call(()).await
}).await
}
```
This code will not compile because `spawn` requires that its future is `Send` and the future returned by `call` is not known to be send, and there is no way to even name it.
It is possible to use the `trait-variant` crate today to make a `Send` version of a trait. This involves adding an attribute like `#[trait_variant::make(SendService: Send)]` to the trait definition above. The variant trait generated by the macro will look like this:
```rust!
trait SendService<Request>: Send {
// ^^^^^^
type Response;
fn call(&self, req: Request) -> impl Future<Output = Self::Response> + Send;
// ^^^^^^
}
```
The `SendService` trait alias works fine for implementations that will always be `Send` or not, but it breaks down if you want to implement the *layer* pattern of wrapping a generic service (also known as middleware):
```rust
pub struct LogService<S>(S);
impl<S, Request> Service<Request> for LogService<S>
// ^^^^^^^ Should this be Service or SendService? Depends on S...
where
S: Service<Request>,
Request: fmt::Debug,
{
type Response = S::Response;
async fn call(&self, request: Request) -> Self::Response {
println!("request = {:?}", request);
self.service.call(request).await
}
}
```
If you try to do this you are stuck. The trait-variant macro generates a blanket impl so that any `SendService` type also implements `Service`, which means users only ever have to implement one of the traits. But this prevents more generic types from having an impl for each because one of them will overlap with the blanket impl.
## Secondary pattern: Bounds on refining impls
This problem is not limited to Send bounds or even auto traits. Consider the `IntoIterator` trait introduced in the [RPITIT RFC](https://rust-lang.github.io/rfcs/3425-return-position-impl-trait-in-traits.html):
```rust
trait IntoIterator {
type Item;
fn into_iter(self) -> impl Iterator<Item = Self::Item>;
}
```
A user may want to write a function that takes `impl IntoIterator`, but additionally requires that the iterator implements `DoubleEndedIterator`. With the RPITIT version of the trait, there is currently no way to do so.
```rust
fn show_reverse<I: IntoIterator>(iter: I)
where
I::Item: Debug,
/* what should we put here? */
for i in iter.into_iter().rev() {
// ERROR ^^^^^^
dbg!(i);
}
}
```
## Zooming out: Inflexibility of RPIT
Taking a step back, we can see that there is a big gap in functionality between return-position `impl Trait` and named type aliases. This necessitates the use of the `trait-variant` crate, which as we've seen is only a partial solution. Furthermore, users who run into this gap can't necessarily do anything about it, because they might not control the code where the return-position `impl Trait` is defined. This gap is the reason we discourage the use of RPIT in public APIs overall. It is also the reason we have issued a warning against using `async fn` in public traits, even as we announced its stabilization.
We should fix this gap for two reasons:
1. Without stable TAITs, users who need opaque or unnameable types have no other recourse.
2. Even if TAITs were stable, we shouldn't have a feature that is discouraged for use in such broad contexts as "any public API" for a long time.
Last year we included a solution to this problem as part of the "minimum viable product" for async fn in trait and RPITIT, but relented when we failed to garner full team consensus on a solution. A number of users commented on the AFIT/RPITIT stabilization announcement that the feature felt unfinished for this reason. It's time to go back and finish what we started.
## Design axioms
We believe...
* **Users should be able to write and reuse middleware that passes through its auto traits.** Our type system needs to be flexible enough to support this pattern.
* **Users want uncluttered trait definition and impls that can just say `async fn`.** We want a trait like `Service` to be as close as possible to `trait Service { async fn call() }` and to be implementable via something like `impl Service for Foo { async fn call() { } }`. It's a negative if we have to add extra annotations to make a trait definition widely reusable.
* **The "send bound" problem (or a close relative) is just a special case of a more general problem of bounding or naming RPIT return types (especially with traits).** Therefore, we'd like a solution that isn't specific to async functions or the trait `Send` and which permits users to bound specific methods in different ways (not have a single bound that applies to all methods in the trait); ideally it would also generalize to support naming return types from top-level or inherent async functions, though those can be handled with TAITs.
* **Send bounds should be easy to read and write and have a clear meaning.** This is true even if they are hidden by trait aliases, because those aliases have to be read and written.
* **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.
* **It is important to move quickly,** and therefore we bias against designs that require more than one proposal.
## The space of proposals
The question then is how to write a where-clause that means "implements `Service` but where `call` returns a `Send` future". There have been four *kinds* of proposals raised up:
### Trait transformers
One branch of proposals is [trait transformers](https://smallcultfollowing.com/babysteps/blog/2023/03/03/trait-transformers-send-bounds-part-3/). The idea is that people can write `Send Trait` to mean "the trait `Trait` but where each RPITIT/async-fn and `Self` have an additional `Send` bound".
This approach is attractive in its simplicity, but it is less flexible than what we typically expect of a core type system feature in Rust. It isn't clear whether we want transformers other than `Send`, or if we always want exactly this behavior for `Send`, or for other traits. Finally, there is no obvious way to generalize this to free functions from traits. For these reasons, it doesn't do much to solve the more general problem of the limitations of RPIT. Overall, this approach doesn't accord with our design axiom that **the "send bound" problem (or a close relative) is just a special case of a more general problem of bounding or naming RPIT return types**.
Trait transformers may be a good extension to the language in the future. Given any of the other approaches, users can make trait aliases like `SendTrait` (either via proc macros or hand-coding). Seeing whether this becomes a common pattern in the ecosystem would be highly informative for designing a trait transformers feature in the future.
### Associated types
Another branch of ideas is to make the return type be "just" an associated type. This associated type could be either explicit or implicit. There are many variations here.
#### Explicit associated type
This approach would let the user defining the trait give an *explicit name* to the future that gets returned. In this case, the where clause could be written as *something* like
```rust
async fn spawn_call<S>(service: S) -> S::Response
where
S: Service<()> + Send + 'static,
for<'a> S::Call<'a>: Send, // <-- this
```
where the user has somehow annotated the trait to indicate that the return type of `call` should be stored in an associated type `Call`. Whatever syntax we use for this on the trait side, it would effectively *desugar* to something like
```rust
trait Service<Request> {
type Response;
type Call<'a>: Future<Output = Self::Response>;
fn call(&self) -> Self::Call<'_>;
}
```
There are several variations of this, covered in the alternatives. All of them have the advantage that the where-clause added to `spawn_call` is "just" an associated type. But they also share a common disadvantage, which is that the trait must be annotated in *some* way to indicate the name we will use for the associated type (although it could conceivably be automatically chosen via a procedural macro). It must also name every generic parameter that may be captured by the return type.
By and large we have rejected these proposals for falling afoul of our design axioms that **users want an uncluttered trait definition** and **users want an uncluttered impl**. It's not exactly *impossible* to achieve these goals syntactically, but doing so requires a set of other features, violating our design axiom that **it is important to move quickly**.
#### Explicit associated type supported by a proc macro
When we released RPITIT/AFIT, we also released a [`trait-variant`](https://crates.io/crates/trait-variant) crate that automates the trait transformer pattern. We could take a similar approach here, building on top of ATPIT which we expect to stabilize soon.
With such a supporting crate, users could write trait definitions such as:
```rust
#[rtnify] // Bikeshed name
trait Service<Request> {
type Response;
// Invoke the service.
async fn call(&self, req: Request) -> Self::Response;
}
```
And we would convert that into, e.g.:
```rust
trait Service<Request> {
type Response;
type Call: Future<Output = Self::Response>;
fn call(&self, req: Request) -> Self::Call;
}
```
The proc macro could support an attribute on the function to change the name of the associated type, and it could similarly transform trait impl blocks.
Without such transformation, currently, when writing impls, users would need to wrap the body in an `async { .. }` block to use this, however it's conceivable we could add sufficient support for refinement or inferred associated types such that AFIT could be used in the impl instead.
Writing these impls could also be a use case for the `fn foo() -> .. = async { .. }` syntax we have discussed. E.g.:
```rust
impl<R> Service<R> for () {
type Request = ();
type Call = impl Future<Output = Self::Request>;
fn call(&self, req: Request) -> Self::Call = async {
todo!()
}
}
```
#### Implicit associated types
Building on the previous approach, another approach is to implicitly create associated types for each method (or at least, each method using RPITIT).
There are two natural choices for the name to use:
* The associated type could have exactly the same name as the method. This works because Rust has separate type and value namespaces.
* The associated type could be the name of the method converted to UpperCamelCase.
Either of these options might be confusing to the user. The first is somewhat ambiguous because it isn't immediately clear if we are describing the method itself or its return type. Incidentally, we might want to reserve this syntactic space for describing methods and functions one day (so we could, for instance, write a bound that a method is `const`, e.g., `Service<call: const>`). The second is a more arbitrary and opinionated rule than Rust tends to use in its core language, and contradicts our design axiom that **the Rust language shouldn't make opinionated choices about names etc**.
Any option we pick runs the risk of interfering with existing associated types of the same name. In those cases we would be forced to either not generate an associated type, or have the user annotate their method with the associated type name they want.
Another place where this proposal is forced to make opinionated choices (and thus contradict our design axiom) is when deciding what type/lifetime parameters the associated type should have. For example, consider what parameters the associated should type have for the following method, and in what order. How would your answer change if there were explicit generic parameters in addition to implicit ones?
```rust!
trait Foo {
fn foo(&self, x: impl Debug, y: &i32) -> impl Debug;
}
```
One option would be to introduce `type Foo<'a, T, 'b>`, but it is also possible to use only a single lifetime parameter to represent both `&self` and `&i32`, since those parameters only appear in contravariant position -- in fact, many methods in practice could get by with a single lifetime parameter. These choices matter a lot when users have to write bounds, e.g., `where for<'a, 'b, T> F::foo<'a, 'b, T>: Send` (arguably this design contradicts the axiom **Send bounds should be easy to read and write and have a clear meaning**).
### Function as a type
Rather than putting an associated type representing the return type directly in our trait, we could instead have the user treat the function *itself* as a type to be projected from. For instance:
```rust
async fn spawn_call<S>(service: S) -> S::Response
where
S: Service<()> + Send + 'static,
for<'a> S::call::<'a>::Output: Send,
```
The idea is that every function has a type unique to that function. This type implements a subset of the `Fn*` traits, and therefore it is reasonable to project `::Output` from it.
This proposal is equivalent to defining an associated type for each function with the same name as the function, except the associated type describes the function itself and is always zero-sized.
This approach is more flexible than the previous approach, in that it leaves space for us to use traits for describing functions directly in the future. On the other hand, it inherits the need for HRTB, and `::Output` makes it even less ergonomic to write bounds with.
It also suffers from an unfortunate ambiguity of meaning. Consider two ways of writing our trait from above.
```rust
trait Service<Request> {
type Response;
async fn call(&self, req: Request) -> Self::Response;
// ^^ can also be written as:
fn call(&self, req: Request) -> impl Future<Output = Self::Response>;
}
```
**If I see a bound like `S::call::Output: Send`, how do I know which `Output` associated type am I referring to: The one from `FnOnce`, or the one from `Future`?** This lack of clarity around `Output` contradicts the axiom **Send bounds should [..] have a clear meaning**.
It would certainly be reasonable for a user to expect that this is the output of the `Future`, given how visible that associated type is in async-related code. In contrast, the `Output` associated type of `FnOnce` is almost never seen in actual code, because we require specifying those bounds using the `FnOnce() -> T` syntax.
### Return type notation
The final branch of proposals gives the user the ability to directly bound the return type of a method or function returning RPIT using a special syntax. There are some details to work out in the syntax, but it might result in code that looks something like this..
```rust
async fn spawn_call<S>(service: S) -> S::Response
where
S: Service<()> + Send + 'static,
S::call(): Send,
```
In conjunction with inline associated type bounds, we might shorten the above where clauses to a single line:
```rust
async fn spawn_call<S>(service: S) -> S::Response
where
S: Service<(), call(): Send> + Send + 'static,
```
This approach sidesteps all of the issues mentioned previously:
* It doesn't add any cruft to the trait or impl.
* It doesn't do anything "sneaky" or ambiguous by defining implicit items in the trait itself.
* Writing bounds with it is clean; in particular, it doesn't require explicit HRTB or some general mechanism for doing that implicitly.
* It's perfectly clear how to extend the syntax for talking about generic parameters (as we will show).
* It reserves syntactic space for writing bounds on functions directly, rather than talking about their return types.
While the addition of special syntax is significant, we think this extension is justified (and, in part, motivated) by the existence of the `FnOnce() -> T` syntax.
As we will explain further below, we think return type notation is the most promising option, but it comes with a few prickly questions of its own.
## Return type notation syntax considerations
There are a few tradeoffs to consider with respect to the RTN syntax.
* Extensibility
* Ease of reading
* Ease of writing
### Extensibility
Among the proposals we've considered so far, RTN ranks the highest in extensibility. Not only does it leave space for bounding functions themselves, it also allows us to apply elision/HRTB rules that may turn out only to make sense for function calls.
### Ease of writing
Among the proposals we've considered so far, RTN is the second easiest to write, behind trait transformers which can be considered later.
However, in user studies we found that for traits with multiple async methods, it was still too verbose to be used directly. In these cases, trait aliases (either actual trait aliases, an unstable feature, or simulated trait aliases using super traits) would be used.
### Ease of reading
We argue that RTN is the easiest to read of the proposals considered so far (with the possible exception of trait transformers), for the following reasons:
* It has special syntax that signifies something different is going on relating to the invocation of the function.
* That special syntax is relatively succinct.
* That special syntax is suggestive of its meaning by resembling function call syntax.
The last point is also a weakness of the proposal, in that it's possible to be confused about what the syntax means in different contexts.
It's not obvious though that having types and expressions look like one another is a problem. In fact, we consciously *tried* to make constructors and their types line up in other cases, e.g. `(id1, id2)` or `[id1; n]`. While no doubt some users will find notation like `T: HealthCheck<check(): Send>` confusing, we've not seen a lot of evidence of this so far in our ancedotal experimentation.
One particularly interesting example is type vs const generics. For example `T<foo()>` and `T<{foo()}>` would have quite different interpretations. Today the proposal does not permit one to write `T<foo()>`, but one could imagine it, perhaps only in where-clauses like `where Rc<T::m()>: Something`. In cases like this, the compiler can easily detect the mistake and suggest adding (or removing) the `{}`.
#### Kinds of function call-like syntax
Position | Example | Creates | Syntactic cues |
-----------|----------------------|---------|----------------|
Expression | `service.call()` | Value | Inside curly braces
Type (RTN) | `S::call()` | Type | Inside angle brackets, `:` after bindings, Types or potentially pattern syntax like `..`, `_`, inside parens, A `:` comes after the type specifying a bound
Trait bound| `F: FnOnce() -> i32` | Bound | `where`, `:` after a type, Known trait names with CamelCase, Types inside parens
## What we aim to decide
* Do we agree that within the space of proposals, something like "return type notation" is ultimately the right approach?
* What are the points of consensus and disagreement around syntax?
## Our proposal
For today, we propose to ship an "MVP" of RTN that
* only supports functions with lifetime parameters (not type parameters);
* e.g., `async fn foo<'a>(x: &'a u32)` is ok, not `async fn foo<T>(x: T)`
* supports quantification over all parameters, either with `()` or `(..)` notation, but not specifying the type of particular parameters;
* e.g., `T::send(): Send` or maybe `T::send(..)` is ok, but `T::foo(SomeType): Send` is not
* works anywhere you can write a bound;
* e.g., `where T::method()`, `dyn Foo<method(): Send>`
* works anywhere you can write a type;
* e.g., `let x: T::method() = x.method()`
* supports bounding by any trait;
* supports bounding only RPIT[IT] functions;
* supports both trait methods and free functions.
Parts of this may not actually be in the MVP we stabilize, but we are not aware of further design questions to work out.
For the future, we will eventually support
* Quantifying over type parameters.
* Specifying specific types and lifetimes in the generics and argument list, probably.
## Return type notation positions and limitations
Our proposal means that, eventually, we would support RTN in all of the following positions:
* Where clauses: `where <S as Service>::call(): Send`
* Inline bounds: `S: Service<call(): Send>`
* Types
* `let fut: Service::call() = s.call();`
* `let futs: Vec<Service::call()> = vec![s.call()];`
* `fn takes_service_call(fut: Service::call()) { ... }`
* ```rust
struct WrapsService<S: Service> {
service: S,
fut: Option<S::call()>,
}
```
* Referencing free functions e.g. `foo()` to refer to the return type of `async fn foo()`
* Allowing bounds for arbitrary traits (not just auto traits)
* Should we allow bounds for all traits, or only auto traits?
Possible future extensions include:
* RTN for non-RPIT(IT) functions. We decided to rule this out for now as it has limited utility.
One thing we do not expect to ever support is using RTN to return to the return type of callable items found in local variables. Hence it cannot be used to name the return type of closures, as in the following example. Supporting this would get us right into the territory of dependent types and bring a lot of complexity for narrow benefit:
```rust
let foo = || s.call();
let output: foo() = foo();
```
## Alternatives
How would we make the associated type or fn projections work?
* Elided parameters in where clauses are HRTB (where specifically? refer to the discussion about anonymous lifetimes on impl Trait)
* Attribute to specify assoc type name
* Or, inferred assoc types
## Future language features
This is how we see RTN fitting into the bigger picture. In particular, we can see a path to removing much of the remaining "cruft" users have to think about while improving flexibility.
### Implementable trait aliases
This allows users to have an `impl SendService for MyType`. Alternatively, if the traits are `Service` and `LocalService` (as Tower has said they will do), `impl Service for MyType` or `impl LocalService for MyType`.
### Trait transformers
As previously mentioned, we can add trait transformers to the language when we are more confident about their use cases and the set of functionality we want to support.
#### Auto trait bounds as effects
If we add some kind of effects to the language, it is conceivable that we could make `Send`-ability some kind of effect and apply it as a trait transformer, e.g.
```rust
fn foo<effect ?Send>(s: impl ?Send Service) -> impl ?Send Service { ... }
```
### Removal of auto trait leakage
This proposal is compatible with a path to remove auto trait leakage. It would require implication bounds, like
```rust
fn foo<S: Service>(s: S) -> impl Service + (Send if S::call(): Send) { ... }
```
(The use cases for implication bounds overlap significantly with the use cases for auto trait effects, but we would still need implication bounds for backwards compatibility.)
### Scoped spawn APIs
Assuming we end up with trait transformers (or everyone just uses trait aliases), there's one remaining "wart" in our example signatures: The `'static` bound. Scoped spawn APIs would allow us to remove that.
## References
* [May 25, 2024 Design Meeting on RTN](https://github.com/rust-lang/lang-team/blob/master/design-meeting-minutes/2023-05-24-return-type-notation.md)
* [RPITIT RFC](https://rust-lang.github.io/rfcs/3425-return-position-impl-trait-in-traits.html)
* [Stabilization report for AFIT and RPITIT](https://github.com/rust-lang/rust/pull/115822)
---
# Discussion
## Attendance
- People: TC, tmandry, pnkfelix, Josh, eholk, nikomatsakis
## Meeting roles
- Minutes, driver: TC
## Experimental result: More than just `Send` bounds
TC: This is just an observation. I recently did an experiment to show that we could provide an AFIT version of the asynchronous iterator trait and provide a blanket `IntoAsyncIterator` implemetation for it to turn it into a "real" `poll_next`-based `AsyncIterator`. I used ATPIT when defining the traits, and I found that being able to name and bound all of the types returned by the methods to generic parameters in the blanket impl was critical to making this possible.
The experiment is described [here](https://rust-lang.zulipchat.com/#narrow/stream/187312-wg-async/topic/.60AsyncIterator.60.20rename.20meeting.202024-01-22.3F/near/417391487), and the annotated demonstration code is [here](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=be3ec7d9d52e597a4dfe38bbddd89e34).
tmandry: This shows also the need for RTN in type position.
## Speed
scottmcm: why is it particularly important "to move quickly" on this one any differently from any other feature? Sure, we'd like to keep moving, but an axiom for "quickly" is odd to me. (Why here and not move quickly on specialization, say?)
TC: Potentially an `#[rtnify]` (bikeshed name) proc macro as described [here](#Explicit-associated-type-supported-by-a-proc-macro), in combination with stable ATPIT, could relieve some of the time pressure.
tmandry: My feeling is that we shouldn't go with a proposal that will take more than a year. The most negative feedback we received on the stabilization of RPITIT/AFIT was about the lack of RTN. I'd convinced myself that proc macros would be OK, but the feedback is making me reconsider.
eholk: The feedback I've received from internal users has reinforced this for me.
scottmcm: How much of this is a general point that we should ship slower? So the feedback might be interpreted that we shouldn't do something too quick here.
NM: I have strong concerns about what scottmcm said. But we should discuss that separately. I want to be sure we keep moving.
NM: You asked "why this and not specialization". There are a lot of people using this feature. Much of the async ecosystem is blocked on this. So essentially the reason is that specialization isn't that important and this is.
(scottmcm: sorry, I phrased it poorly -- not saying this is less important that specialization, or anything, just that one could also write an RFC saying that axiom for stabilizing, say, `min_specialization` today because we should move quickly and we seem not interested in doing something like that.)
Josh: Are we making the right call by linting on the use of this in public? As I understand it, we're doing that because a subset of users have said they're afraid of seeing hardcoded `Send` bounds showing up in various public APIs. However, is that so horrific that we should lint on it in public APIs? If we stop linting that, does the urgency go away?
pnkfelix: My main feeling is that I worry we're going to go down a rabbit hole on this discussion. We should look at what's really driving our timelines. Is it really a date, or is it relative prioritization.
Josh: It sounds like the urgency may be how much is blocked on this, and thus in the queue behind it. (e.g. the use of AFIT for async traits in std, like AsyncRead/AsyncWrite)
eholk: To scottmcm's point about slowing down, I think in this case, RTN has been baking for a while. It hasn't changed recently in any major ways.
Josh: @eholk As a counterpoint to that, just because a proposal has been around for a while doesn't mean it's the right proposal, and we shouldn't rush the evaluation of a proposal on that basis. It's been around for a while, but that doesn't mean it has consensus for approval.
NM: From my point of view, the people I want to unblock are libraries like Tower. It's more than just about that they may need to make some breaking changes. It's about allowing the foundations of the ecosystem to be authored. And these are the parts where we don't want breaking changes.
NM: I'm worried about solutions that landing e.g. three new RFCs. To my mind, that's a significant cost that we want to take into account. But of course we want to do the right thing and take the time needed to do that.
tmandry: We're not blocking just on niche use cases here.
Josh: There's the unrelated issue here that it's hard to specify Send bounds when using `async fn` generally.
tmandry: The lint is that you may have forgetten to add them.
Josh: Right, but we can help with that separately, and make it easier to specify the `Send` bounds, which is separate from the problem of making the same trait work with both Send and non-Send.
NM: What tmandry is trying to say, I believe, is that one could author a crate in one way, but then downstream, people could run into problems. We want the downstreams to be able to fix their own problems. Also, that decision is done. We stabilized it.
Josh: We should separate the problem of "hey, you may have gotten this wrong in a public API", but that's different than the ability to write a Send bound.
NM: I don't want it to be wrong for people to write the trait the way they wrote it.
tmandry: Part of the motivation is to have a way to write a service that is generic over whether you want the bound or not.
TC: ATPIT does unblock some of this, strictly speaking, but it doesn't address many of the points that nikomatsakis raised, such as allowing downstreams to solve their own problems.
## Trait Transformers generality?
pnkfelix: Regarding, "It isn’t clear whether we want transformers other than Send", can I flip that on its head and ask: Is there a way to encode the example of "IntoIterator where the returned Iterator: DoubleEndedIterator" as a trait transformer? If so, then there's your example of another usage.
pnkfelix: From skimming the associated blog post from Niko, it seems like it could be encoded as a trait transformer (but I'm not yet sure I understand the trait tranformer proposal well enough to answer the question of whether it is possible... and I'm pretty sure that even if it were possible, it wouldn't be the ideal outcome for this use case....).
tmandry: I haven't been thinking we would want to encode that as a transformer, which speaks to the need for a more general feature.
## Trait consumer side versus trait producer side
Josh: It feels like RTN, and for that matter trait aliases, are about expressing bounds wherever the trait is *consumed*. By comparison, defining something like `LocalService` or `ServiceSend` or similar is producer-side: the definition of the trait anticipates that the user might want a specific variant. I think, in practice, it's going to be excessively verbose to use RTN or similar for any trait that has more than one function, and even for one function it's more verbose than `ServiceSend` or equivalent would be.
Josh: I also think, in practice, we're not going to run into cases of "I need *this* method to return Send but it'd be a problem to restrict this *other* method to return Send"; "I need all methods to return Send" should suffice.
Josh: I think we should be proposing a mechanism that encourages the definition of common trait variants, rather than having every consumer of the trait define what they want. That might be trait transformers, or something else like it. I think we should *not* be defining a syntax that works in arbitrary bounds, and thus encouraging the proliferation of such bounds.
Josh: `S: Service<(), call(): Send> + Send + 'static` does not seem like something people should be writing in more than one place.
TC: It's also worth noting in this context that ATPIT will give trait authors control at the definition site of the amount of flexibility they want to give the use site via which types are expressed as associated types that can be implemented using ATPIT.
NM: The point Josh made doesn't address the point about how to write middleware.
tmandry: I don't think there's really disagreement on this point about repeatedly writing bounds. If we have to write this bound over and over, we'll want to have trait aliases.
NM: What I'm hearing, Josh, is that in your use case you'd prefer an alias. We're saying that yes, you should be able to do that. But there are also cases where an alias doesn't work, such as for the middleware. We want to address that case.
Josh: I don't understand why it doesn't work for that use case.
NM: You have to commit upfront.
scottmcm: They're the same trait, so you can't unconditionally implement both.
Josh: You're right that's more complicated if you have a trait for middleware.
NM: There are problems even for impls on types.
## Would generics work for bounds?
Josh: Hypothetically, could we have:
```rust
trait Service<Bound = ?Send> {
fn call(&self, req: Request) -> impl Future<Output = Self::Response> + Bound
}
```
And then people can write `Service` or `Service<Send>`? That seems like it'd get us the same effect as "trait transformers" but with the trait designer able to specify *precisely* where the "transforming bounds" go.
Josh: (This is leaving aside the problem of how to specify a bound on the return type of an `async fn`. Grumble grumble "should have made the Future visible" grumble. :) )
tmandry: Yeah, that gets to the "auto trait bounds as effects" at the end of the doc. Another form is "traits as generics". But these are far off in terms of ability to impement.
Josh: @tmandry Can you elaborate on "far off in terms of ability to implement"? What makes it especially difficult to accept something like `Send` or `?Send` in a generic bound and apply it in the places the trait puts it?
tmandry: I'll defer to Niko or someone on the types team for this, but I imagine that introducing a new kind of generic parameter to the type system, for traits, is not a trivial change.
Josh: I'd love to know if there is a restricted version of this that would be sufficiently easy to implement. We already allow bounds like `impl Trait<AssocType = T>, T: Bound`, and we have an unstable mechanism to allow `impl Trait<AssocType: Bound>`. It seems like we could desugar `Service<Send>` to `Service` plus the same bounds we'd have used for RTN, for everything other than `impl Service<Send>`, and we could defer that part initially until we have things like implementable trait aliases.
## Thinking generally
NM: Part of the angle on RTN here is that it gives us a path forward on solving the problems even with e.g. free functions in APIs that use RPIT.
## Straw poll
NM: I'd like a straw poll.
TC: I know that NM and tmandy on +1 on the proposal.
pnkfelix: I have no objection to RTN. Even what Josh is proposing here wouldn't replace the need for RTN.
Josh: I'd like to know what use cases are not covered by other solutions.
Josh: I'm currently -1 on this proposal otherwise, though my position might change if it turns out there's no other way to solve the use case. I don't want this to propagate throughout the ecosystem.
* I would like to see better examples for needing the *full* generality that warrants RTN rather than something simpler (what pnkfelix referred to above)
* Too general, I would prefer not to see people writing multiple bounds on multiple functions
* If we are going to hide it behind an alias, do we really need this to be the syntax it expands to?
scottmcm: I like the general idea of being able to have syntax for the type of a thing. I have some minor syntactic concerns. As long as this extends well to the things not included in the MVP, I'm +1.
Josh: My -1 is on stabilizing it in its full generality in this form. Maybe we could hide it behind a proc macro to limit it in the ways that I've proposed.
TC: How strongly do you feel about your counterproposal? Do you want to write that out for discussion.
Josh: If it passes a smoke-test for basic viability, I'd be happy to write it up.
scottmcm: One of the things I like about RTN is how it desugars to a TAIT whose name I didn't really care about. So there is use for this as a generally-useful thing even outside of the Send problem.
Josh: I'd sooner do RTN than to tell people that they should have to name all associated types somewhere.
(The meeting ended here.)
---
## Restriction to traits
scottmcm: it seems like this is only talking about associated types in traits? Is there no case where people want to be able to bound on the return types of things like normal functions? Is there a syntax that might make sense that's not tied to being inside a trait's fake generics for associated types?
tmandry: Yes, the full proposal includes support for bounding RPIT of free functions.
scottmcm: So does the MVP allow things like `let x: foo();` as RTN for the return type of that functions? That seems like a very small edit distance from `let x = foo();`...
tmandry: Yes, the proposal includes this, though only for functions returning `impl Trait`.
Josh: Is there a use case for such bounds, or just a desire for full generality?
tmandry: There is a use case for RTN in type position, though I've only seen it in structs and perhaps function signatures. For let bindings, I could imagine something macro related.
Josh: I meant, is there a use case for it on free functions, rather than traits?
tmandry: I mean the generic parameter *is* a trait (or trait bound)
scottmcm: There's certainly a desire for the "pass through `ExactSizeIterator` if the input is" kinds of things in free functions, which feels an awful lot like the Send bound problem to me. That's the biggest reason people are using named iterators instead of `impl Iterator` that I know of today, for example, especially if we get TAITs.
## ~~Associated type full generality~~
~~pnkfelix: Just so I'm clear, when you write: "where the user has somehow annotated the trait to indicate that the return type of call should be stored in an associated type Call" -- in general, due to the potential for a method to have type parameters, we will have to deal with the case where expressing the return type here will require instantiating those hypothetical type parameters? As in: `S::Call<Actual1, Actual2>` to describe the return type of `fn call<G1, G2>(...) -> ...`, when instantiated at G1=Actual1 and G2=Actual2, right?~~
~~pnkfelix: (It seems entirely doable, I just want to anticipate the need to express it in the general case.)~~
pnkfelix: (Never mind; the text itself confirmed this.)
## Disambiguating Function as a type
pnkfelix: One way to deal with the `fn`/`async fn` ambiguity for FunctionAsAType (or maybe "Function as a type-path component?") is to allow (or even *require*) the use of `fn` or `async fn` in the path syntax itself. `S::fn call::<...>` would always be interpreted as a non-async-sugared form, while `S::async fn call::<...>` would always be interpreted as async sugar (even if `call` were declared without such sugar) that would always hide the underlying `Future` and jump straight to the declared return type.
tmandry: Actually we did consider this at one point. I think the idea of having a space inside a path was considered too surprising and difficult to parse (for humans).
## Doesn't "Return type notation" fall into same problem as Fn as a Type?
pnkfelix: How do I know whether `S::call(): Send` denotes a constraint on the Future, or a constraint on the associated Output type of the Future?
pnkfelix: If its the latter, how do I write the former? (And vice versa: if its the former, how do I write the latter? Perhaps `S::call().await: Send`?)
nikomatsakis: my opinion -- this will probably be confusing-ish, but ultimately, when you do `s.call()`, you get a future, and so this behaves the same. And yes `.await` might be useful at some point in the future.
## Trait bounds in traits
scottmcm: `where T: Iterator<Item: std::fmt::Debug>` is currently unstable. Should that be stabilized with this, if we're stablizing `where T: Foo<bar(): Debug>`?