-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Return position impl Trait
in traits
#3425
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
I don't think this should have a T-types FCP for the feature (perhaps for stabilization). @rustbot label -T-types |
This comment was marked as off-topic.
This comment was marked as off-topic.
@rfcbot fcp merge This feature has been a long time coming and feels like a no brainer to me. I'm going to start the merge proceedings. |
Team member @nikomatsakis has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns. |
# Motivation | ||
[motivation]: #motivation | ||
|
||
The `impl Trait` syntax is currently accepted in a variety of places within the Rust language to mean "some type that implements `Trait`" (for an overview, see the [explainer] from the impl trait initiative). For function arguments, `impl Trait` is [equivalent to a generic parameter][apit] and it is accepted in all kinds of functions (free functions, inherent impls, traits, and trait impls). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's this ...
XXX document:
- you can capture type parameters
- but not lifetimes, unless they appear in the bounds
in the explainer. It would be nice to explain a bit better that + what happens if all the lifetimes are captured as we do with types and consts too.
Wrote this down after midnight so it's not as clear and concise as I would have liked. I only really thought about this in-depth after Niko proposed the FCP. tl;dr: RPITIT is not a trivial extension of TAIT or RPIT and does not simply fall out of the desire for a consistent design. As I stated separately, I don't think we should add return type notation RTN. This causes RPITIT to be a major footgun for library authors without a significant benefit. I very much agree that we should add support to RPITITs are not simple opaque typesmoved to a hackmd as it is somewhat pedantic I don't agree that RPITIT will result in a more consistent language given that the semantics of RPITIT are significantly different from RPIT. It is easy to provide context to the error message when trying to use it, recommending an associated type with TAIT. RPITIT can result in worse library designTo keep this short, I prefer https://github.com/tmandry/rfcs/blob/rpitit/text/0000-return-position-impl-trait-in-traits.md#what-about-using-an-explicit-associated-type over RPITIT and don't think we should add RPITIT (at least for now). The RFC uses Going through the list of cases where an explicit associated type should be used, we can reverse it to get requirements to correctly use RPITIT:
We have proof that the second and third point do not hold for More generally, I cannot think of a trait where RPITIT would be appropriate given these requirements, unless the trait is local to your library or sealed. I assume that given the ability, library users will sometimes end up using RPITIT simply because it is possible, which will then require breaking changes to fix later on. RPITIT is not necessary for async functions in traitsBy going with the "explicit associated type" route, we can instead add some attribute to express the concept of "this named associated type may be implicitly defined by this function", something like trait IntoIterator {
type Item;
type IntoIter: Iterator<Item = Self::Item>;
#[may_implicitly_define(IntoIter)]
fn into_iter(self) -> Self::IntoIter;
}
impl<T: Iterator> IntoIterator for T {
type Item = T::Item;
// does not explicitly define `IntoIter`,
fn into_iter(self) -> Self::IntoIter {
self
}
// or
fn into_iter(self) -> impl Iterator<Item = T::Item> {
self
}
} A similar approach can then be used for RPITIT is at most a linear ergonomics improvementAs stated in the RFC, RPITIT should be equivalent to its desugaring using TAIT and associated types. This desugaring has some repetition but it should pretty much never significantly impact development speed or readability. Given the fairly mechanical desugaring, we should be able to emulate RPITIT using TAIT and a set of macros for trait definitions and impls. If the manual desugaring poses a major issue, users should be drawn to such a set of macros, at which point we can reconsider adding RPITIT. |
I agree with @lcnr that this feature effectively requires return type notation. In some hypothetical future where Rust decided against adding return type notation, RPITIT would clearly not hold its weight. Therefore it does seem strange to be landing it without at least talking about return type notation. Unlike @lcnr, I have no complaints about return type notation; I think it sounds great. But I am confused about the details. Is it going to be |
Is deferring this acceptable? It seems like it should be a core part of the proposal. Then again, we still have no syntax for naming the return type of a closure or a regular Should we at least have somewhere to discuss relevant issues:
|
An alternative approach: trait T {
auto type Fut = return_type_of<Self::fun>;
async fn fun(&self) -> i32;
}
Putting both of these together would allow e.g. migrating Addendum: but is an So the above is just:
|
As a user of stable Rust, I'd like to see effort put first into stabilization of TAIT, which would enable many of the use cases that RPITIT would, and also provide more information about what benefits RPITIT would or would not provide and how it should be designed, since it results in a smaller delta (“write this trait using an associated type” vs. “can't practically write this trait at all”). |
[summary]: #summary | ||
|
||
* Permit `impl Trait` in fn return position within traits and trait impls. | ||
* Allow `async fn` in traits and trait impls to be used interchangeably with its equivalent `impl Trait` desugaring. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One consequence of this is that it basically stabilizes the impl Future
desugaring. I don't imagine we'll want to change it, and enough things depend on the impl Future
desugaring now that we probably can't change it practically already, but it still means we're committing to what in some ways feel like internal implementation details to me.
|
||
### async fn desugaring | ||
|
||
`async fn` always desugars to a regular function returning `-> impl Future`. When used in a trait, the `async fn` syntax can be used interchangeably with the equivalent desugaring in the trait and trait impl: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably the reason for doing this is so that we can use #[refine]
to make stricter promises on the impl? For example, adding a Send
bound at the impl level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's one (another example of an added promise would be that you don't capture self
). A whole other reason would be to include code that runs at call time instead of during poll
.
🔔 This is now entering its final comment period, as per the review above. 🔔 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned about an inconsistency with inherent impls regarding the capture rules for lifetimes. It can be shown in this example:
struct Foo<'a>(&'a str);
// ERROR: `impl Sized` doesn't capture 'a
impl<'a> Foo<'a> {
fn into(self) -> impl Sized { self }
}
trait IntoImpl {
fn into(self) -> impl Sized;
}
// OK! Allowed by the RFC.
impl<'a> IntoImpl for Foo<'a> {
fn into(self) -> impl Sized { self }
}
We say that a generic parameter is captured if it may appear in the hidden type. These rules are the same as those for -> impl Trait in inherent impls.
Given the difference above, this statement is no longer true.
Additionally, in the capture rules, the text talks only about generic parameters of the trait implementation - it says nothing about the trait definition. It is not clear to me, for example, whether impl Sized
captures the lifetime 'a
here:
trait Trait<'a> {
fn test() -> impl Sized;
}
@aliemjay Oh, I think that text should have been removed. I've reorganized the sections related to scoping rules and removed the rules that were incorrect. RPITIT should now be consistent with RPIT in inherent methods. I also noticed there's an inconsistency in how the implementation handles this; see rust-lang/rust#112194.
We are talking about the hidden type supplied by each implementation. The type supplied by the implementation can't name the parameters of the trait itself because those aren't in scope – instead, usually you would have a generic parameter of the impl that corresponds to each trait parameter. (I realize this is a bit subtle, but in the reference-level sections it helps to be pedantic.)
In this example the return type is not allowed to capture |
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. This will be merged soon. |
The decision here doesn't seem to be straightforward... If it captures the lifetime #![feature(return_position_impl_trait_in_trait)]
trait Trait<'a> {
fn test() -> impl Sized;
}
fn test<'a, 'b, T: for<'x> Trait<'x>>() {
let mut x;
x = <T as Trait<'a>>::test();
x = <T as Trait<'b>>::test();
//^~ ERROR `'a` and `'b` must be the same: replace one with the other
} Consequently we should change the desugaring to involve a supertrait that does not have the lifetime trait SuperTrait {
type AnonOpaque;
}
trait Trait<'a>: SuperTrait {
fn test() -> Self::AnonOpaque;
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default trait functions
impl Trait
in trait functions with default body is underspecified. Is it allowed? If yes, we should specify the desugaring. It is not trivial as it depends on the feature of associated type defaults. We should also mention the drawback of not being able to apply #[refine]
to it.
Legal positions for impl Trait
in trait impls
The text specifies the legal positions in trait definitions but leaves trait implementations unspecified. Do we allow this?:
trait Trait {
fn test() -> impl Sized;
}
impl Trait for () {
fn test() -> Result<impl Sized, impl Sized> { Ok::<(), ()>(()) }
}
Forward compatiblity with named return types
Should we limit the legal positions for impl Trait
to be maximally compatible with the naming schemes for return types?
fn f() -> impl Sized; // Ok.
fn f() -> Box<impl Sized>; // Not ok; cannot be named.
fn f() -> impl Iterator<Item = impl Sized>; // Ok.
fn f() -> impl Iterator<Item = Box<impl Sized>>; // Not ok; cannot name `impl Sized`.
type Item = u32; | ||
|
||
fn into_iter(self) -> impl Iterator<Item = Self::Item> { | ||
self.into_iter() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an ambiguous method resolution with candidates from IntoIterator
and NewIntoIterator
.
There are multiple similar issues below.
|
||
[scoping]: https://rust-lang.github.io/rfcs/1951-expand-impl-trait.html#scoping-for-type-and-lifetime-parameters | ||
|
||
Lifetime parameters not in scope may still be indirectly named by one of the type parameters in scope. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate further? I'm not sure if I understand this correctly. Does that mean we allow impl Sized
in the implementation to capture 'a
because it captures the type parameter Self
in the trait definition?
trait Trait {
fn foo(self) -> impl Sized;
}
impl<'a> Trait for &'a u8 {
fn foo(self) -> impl Sized { self } // Is this ok?
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intent is to be consistent with inherent methods, but the implementation is not currently. I believe this is captured in the unresolved question around rust-lang/rust#112194.
Yes it is; I've updated the text to clarify that. I don't think there are any nonobvious interactions with associated type defaults, given that the types cannot be named in the first place. Not being able to apply
Elaborated what's allowed in the RFC. The short answer is no, but it might be an interesting future possibility.
My initial thought to this was no, but I think it's worth pondering. Added this as an unresolved question.
I think the decision is captured as an unresolved question around rust-lang/rust#112194. I updated the RFC text to point to your comment as well. |
Huzzah! The @rust-lang/lang team has decided to accept this RFC. Tracking issue: rust-lang/rust#91611 If you'd like to keep following the development of this feature, please subscribe to that issue, thanks! :) |
(@aliemjay -- my sense was that the specific points you raised were (a) important details to be resolved but not reasons not to accept the RFC and (b) either captured in unresolved questions or clarified; if you feel otherwise, please follow-up either on tracking issue or zulip) |
Hm, it seems like the tracking issue is shared with async fn in traits. Should we make a separate tracking issue for RPITIT? |
impl Trait
in fn return position within traits and trait impls.async fn
in traits and trait impls to be used interchangeably with its equivalentimpl Trait
desugaring.#[refine]
animpl Trait
return type with added bounds or a concrete type.Background
This RFC is a collaboration between myself and @compiler-errors, and is based on an earlier RFC by @nikomatsakis.
The primary changes from that RFC are:
async fn
is now allowed to be used interchangeably with its equivalentimpl Trait
desugaring.#[refine]
is now included as a way to add more information about the returned type in an impl, either by adding bounds to theimpl Trait
type, or by using a concrete type. Refined trait implementations #3245 did not exist at the time of the previous RFC (return position impl trait in traits #3193), and was added partly in response to discussion there.dyn Trait
on a trait with a method returningimpl Trait
, as long as that method has awhere Self: Sized
bound.Rendered