From ccf4484c25ef74fa23e7505a03b6ca727015acef Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Wed, 10 Nov 2021 09:05:29 -0500 Subject: [PATCH 1/3] first draft of RFC for return position impl trait --- ...00-return-position-impl-trait-in-traits.md | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 text/0000-return-position-impl-trait-in-traits.md diff --git a/text/0000-return-position-impl-trait-in-traits.md b/text/0000-return-position-impl-trait-in-traits.md new file mode 100644 index 00000000000..06d8d96ae38 --- /dev/null +++ b/text/0000-return-position-impl-trait-in-traits.md @@ -0,0 +1,285 @@ +- Feature Name: (fill me in with a unique ident, `my_awesome_feature`) +- Start Date: (fill me in with today's date, YYYY-MM-DD) +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) +- Developed as part of the [impl traits lang team initiative](https://github.com/rust-lang/impl-trait-initiative) + +# Summary +[summary]: #summary + +* Permit `impl Trait` in fn return position within traits and trait impls. +* This desugars to an anonymous associated type. + +# 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). In return position, `impl Trait` [corresponds to an opaque type whose value is inferred][rpit]. In that role, it is currently accepted only in free functions and inherent impls. This RFC extends the support to cover traits and trait impls, just like argument position. + +[explainer]: https://rust-lang.github.io/impl-trait-initiative/explainer.html +[apit]: https://rust-lang.github.io/impl-trait-initiative/explainer/apit.html +[rpit]: https://rust-lang.github.io/impl-trait-initiative/explainer/rpit.html +[rpit_trait]: https://rust-lang.github.io/impl-trait-initiative/explainer/rpit_trait.html + +## Example use case + +The use case for `-> impl Trait` in trait fns is similar to its use in other contexts: traits often wish to return "some type" without specifying the exact type. As a simple example that we will use through the RFC, consider the `NewIntoIterator` trait, which is a variant of the existing `IntoIterator` that uses `impl Iterator` as the return type: + +```rust +trait NewIntoIterator { + type Item; + fn into_iter(self) -> impl Iterator; +} +``` + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +*This section assumes familiarity with the [basic semantics of impl trait in return position][rpit].* + +When you use `impl Trait` as the return type for a function within a trait definition or trait impl, the intent is the same: impls that implement this trait return "some type that implements `Trait`", and users of the trait can only rely on that. However, the desugaring to achieve that effect looks somewhat different than other cases of impl trait in return position. This is because we cannot desugar to a type alias in the surrounding module; we need to desugar to an associated type (effectively, a type alias in the trait). + +Consider the following trait: + +```rust +trait IntoIntIterator { + fn into_int_iter(self) -> impl Iterator; +} +``` + +The semantics of this are analogous to introducing a new associated type within the surrounding trait; + +```rust +trait IntoIntIterator { // desugared + type IntoIntIter: Iterator; + fn into_int_iter(self) -> Self::IntoIntIter; +} +``` + +(In general, this associated type may be generic; it would contain whatever generic parameters are captured per the generic capture rules given previously.) + +This associated type is introduced by the compiler and cannot be named by users. + +The impl for a trait like `IntoIntIterator` must also use `impl Trait` in return position: + +```rust +impl IntoIntIterator for Vec { + fn into_int_iter(self) -> impl Iterator { + self.into_iter() + } +} +``` + +This is equivalent to specify the value of the associated type as an `impl Trait`: + +```rust +impl IntoIntIterator for Vec { + type IntoIntIter = impl Iterator + fn into_int_iter(self) -> Self::IntoIntIter { + self.into_iter() + } +} +``` + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +## Equivalent desugaring for traits + +Each `-> impl Trait` notation appearing in a trait fn return type is desugared to an anonymous associated type; the name of this type is a fresh name that cannot be typed by Rust programmers. In this RFC, we will use the name `$` when illustrating desugarings and the like. + +As a simple example, consider the following (more complex examples follow): + +```rust +trait NewIntoIterator { + type Item; + fn into_iter(self) -> impl Iterator; +} + +// becomes + +trait NewIntoIterator { + type Item; + + type $: Iterator; + + fn into_iter(self) -> ::$; +} +``` + +## Equivalent desugaring for trait impls + +Each `-> impl Trait` notation appearing in a trait impl fn return type is desugared to the same anonymous associated type `$` defined in the trait along with a function that returns it. The value of this associated type `$` is an `impl Trait`. + +```rust +impl NewIntoIterator for Vec { + type Item = u32; + + fn into_iter(self) -> impl Iterator { + self.into_iter() + } +} + +// becomes + +impl NewIntoIterator for Vec { + type Item = u32; + + type $ = impl Iterator; + + fn into_iter(self) -> ::$ { + self.into_iter() + } +} +``` + +## Impl trait must be used in both trait and trait impls + +Using `-> impl Trait` notation in a trait requires that all trait impls also use `-> impl Trait` notation in their retrn types. Similarly, using `-> impl Trait` notation in an impl is only legal if the trait also uses that notation: + +```rust +trait NewIntoIterator { + type Item; + fn into_iter(self) -> impl Iterator; +} + +// OK: +impl NewIntoIterator for Vec { + type Item = u32; + fn into_iter(self) -> impl Iterator { + self.into_iter() + } +} + +// Not OK: +impl NewIntoIterator for Vec { + type Item = u32; + fn into_iter(self) -> vec::IntoIter { + self.into_iter() + } +} +``` + +**Rationale:** Maximizing forwards compatibility. We may wish at some point to permit impls and traits to diverge but there is no reason to do it at this time. + +## Generic parameter capture and GATs + +As with `-> impl Trait` in other kinds of functions, the hidden type for `-> impl Trait` in a trait may reference any of the type or const parameters declared on the impl or the method; it may also reference any lifetime parameters that explicitly appear in the trait bounds ([details](https://rust-lang.github.io/impl-trait-initiative/explainer/rpit_capture.html)). We say that a generic parameter is *captured* if it may appear in the hidden type. + +When desugaring, captured parameters from the method are reflected as generic parameters on the `$` associated type. Furthermore, the `$` associated type has the required brings whatever where clauses are declared on the method into scope (excepting those which reference other parameters that are not captured). This transformation is precisely the same as the one which is applied to other forms of `-> impl Trait`, except that it applies to an associated type and not a top-level type alias. + +Exaample: + +```rust +trait RefIterator for Vec { + type Item<'me> + where + Self: 'me; + + fn iter<'a>(&'a self) -> impl Iterator>; +} + +// Since 'a is named in the bounds, it is captured. +// `RefIterator` thus becomes: + +trait RefIterator for Vec { + type Item<'me> + where + Self: 'me; + + type $<'a>: impl Iterator> + where + Self: 'a; // Implied bound from fn + + fn iter<'a>(&'a self) -> Self::$<'a>; +} +``` + +## Dyn safety + +To start, traits that use `-> impl Trait` will not be considered dyn safe, *even if the method has a `where Self: Sized` bound*. This is because dyn types currently require that all associated types are named, and the `$` type cannot be named. The other reason is that the value of `impl Trait` is often a type that is unique to a specific impl, so even if the `$` type *could* be named, specifying its value would defeat the purpose of the `dyn` type, since it would effectively identify the dynamic type. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +## Can traits migrate from a named associated type to `impl Trait`? + +Not compatibly, no, because they would no longer have a named associated type. + +## Can traits migrate from `impl Trait` to a named associated type? + +Generally yes, but all impls would have to be rewritten. + +## Would there be any way to make it possible to migrate from `impl Trait` to a named associated type compatibly? + +Potentially! There have been proposals to allow the values of associated types that appear in function return types to be inferred from the function declaration. So the trait has `fn method(&self) -> Self::Iter` and the impl has `fn method(&self) -> impl Iterator`, then the impl would also be inferred to have `type Iter = impl Iterator` (and the return type rewritten to reference it). This may be a good idea, but it is not proposed as part of this RFC. + +## What about using a named associated type? + +One alternative under consideration was to use a named associated type instead of the anonymous `$` type. The name could be derived by converting "snake case" methods to "camel case", for example. This has the advantage that users of the trait can refer to the return type by name. + +We decided against this proposal: + +* Introducing a name by converting to camel-case feels surprising and inelegant. +* Return position impl Trait in other kinds of functions doesn't introduce any sort of name for the return type, so it is not analogous. + +There is a need to introduce a mechanism for naming the return type for functions that use `-> impl Trait`; we plan to introduce a second RFC addressing this need uniformly across all kinds of functions. + +As a backwards compatibility note, named associated types could likely be introduced later, although there is always the possibility of users having introduced associated types with the same name. + +## Does auto trait leakage still occur for `-> impl Trait` in traits? + +Yes, so long as the compiler has enough type information to figure out which impl you are using. In other words, given a trait function `SomeTrait::foo`, if you invoke a function `::foo()` where the self type is some generic parameter `T`, then the compiler doesn't really know what impl is being used, so no auto trait leakage can occur. But if you were to invoke `::foo()`, then the compiler could resolve to a specific impl, and hence a specific [impl trait type alias][tait], and auto trait leakage would occur as normal. + +[tait]: https://rust-lang.github.io/impl-trait-initiative/explainer/tait.html + +## Would introducing a named associated type be a breaking change for a trait? + +Converting from returning impl trait to an explicit associated type is a breaking change for impls of the trait. Given this code: + +```rust +trait Foo { + fn bar() -> impl Display; +} + +impl Foo for u32 { + fn bar() -> impl Display { + self + } +} +``` + +transforming it to the following: + +```rust +trait Foo { + type Bar: Display; + fn bar() -> Self::Bar; +} + +impl Foo for u32 { + fn bar() -> impl Display { + self + } +} +``` + +Results in an impl in the `impl Foo for u32`. The [Future possibilities section](#future-possibilities) discusses some possible ways we could mitigate this in the future by other language extensions. + +# Prior art +[prior-art]: #prior-art + +There are a number of crates that do desugaring like this manually or with procedural macros. One notable example is [real-async-trait](https://crates.io/crates/real-async-trait). + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +- None. + +# Future possibilities +[future-possibilities]: #future-possibilities + +We expect to introduce a mechanism for naming the result of `-> impl Trait` return types in a follow-up RFC (see the draft [named function types](https://rust-lang.github.io/impl-trait-initiative/RFCs/named-function-types.html) rfc for the current thinking). + +Similarly, we expect to be introducing language extensions to address the inability to use `-> impl Trait` types with dynamic dispatch. These mechanisms are needed for async fn as well. A good writeup of the challenges can be found on the "challenges" page of the [async fundamentals initiative](https://rust-lang.github.io/async-fundamentals-initiative/evaluation/challenges/dyn_traits.html). + +Finally, it would be possible to introduce a mechanism that allows users to give a name to the associated type that is returned by impl trait. One proposed mechanim is to support an inference mechanism, so that one if you have a function `fn foo() -> Self::Foo` that returns an associated type, the impl only needs to implement the function, and the compiler infers the value of `Foo` from the return type. Another options would be to extend the impl trait syntax generally to let uses give a name to the type alias or parameter that is introduced. \ No newline at end of file From 276a7f15590e83166c58fd98584299a9cbd5a4e3 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Tue, 16 Nov 2021 12:04:03 -0500 Subject: [PATCH 2/3] incorporate comments from RFC --- ...00-return-position-impl-trait-in-traits.md | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/text/0000-return-position-impl-trait-in-traits.md b/text/0000-return-position-impl-trait-in-traits.md index 06d8d96ae38..96967862929 100644 --- a/text/0000-return-position-impl-trait-in-traits.md +++ b/text/0000-return-position-impl-trait-in-traits.md @@ -1,8 +1,8 @@ -- Feature Name: (fill me in with a unique ident, `my_awesome_feature`) -- Start Date: (fill me in with today's date, YYYY-MM-DD) -- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Feature Name: return_position_impl_trait_in_traits +- Start Date: 2021-11-16 +- RFC PR: [rust-lang/rfcs#3193](https://github.com/rust-lang/rfcs/pull/0000) - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) -- Developed as part of the [impl traits lang team initiative](https://github.com/rust-lang/impl-trait-initiative) +- Initiative: [impl trait initiative](https://github.com/rust-lang/impl-trait-initiative) # Summary [summary]: #summary @@ -18,7 +18,6 @@ The `impl Trait` syntax is currently accepted in a variety of places within the [explainer]: https://rust-lang.github.io/impl-trait-initiative/explainer.html [apit]: https://rust-lang.github.io/impl-trait-initiative/explainer/apit.html [rpit]: https://rust-lang.github.io/impl-trait-initiative/explainer/rpit.html -[rpit_trait]: https://rust-lang.github.io/impl-trait-initiative/explainer/rpit_trait.html ## Example use case @@ -69,11 +68,11 @@ impl IntoIntIterator for Vec { } ``` -This is equivalent to specify the value of the associated type as an `impl Trait`: +This is equivalent to the following "desugared" impl (except that the associated type is anonymous): ```rust impl IntoIntIterator for Vec { - type IntoIntIter = impl Iterator + type IntoIntIter = impl Iterator; fn into_int_iter(self) -> Self::IntoIntIter { self.into_iter() } @@ -134,7 +133,7 @@ impl NewIntoIterator for Vec { ## Impl trait must be used in both trait and trait impls -Using `-> impl Trait` notation in a trait requires that all trait impls also use `-> impl Trait` notation in their retrn types. Similarly, using `-> impl Trait` notation in an impl is only legal if the trait also uses that notation: +Using `-> impl Trait` notation in a trait requires that all trait impls also use `-> impl Trait` notation in their return types. Similarly, using `-> impl Trait` notation in an impl is only legal if the trait also uses that notation: ```rust trait NewIntoIterator { @@ -165,9 +164,9 @@ impl NewIntoIterator for Vec { As with `-> impl Trait` in other kinds of functions, the hidden type for `-> impl Trait` in a trait may reference any of the type or const parameters declared on the impl or the method; it may also reference any lifetime parameters that explicitly appear in the trait bounds ([details](https://rust-lang.github.io/impl-trait-initiative/explainer/rpit_capture.html)). We say that a generic parameter is *captured* if it may appear in the hidden type. -When desugaring, captured parameters from the method are reflected as generic parameters on the `$` associated type. Furthermore, the `$` associated type has the required brings whatever where clauses are declared on the method into scope (excepting those which reference other parameters that are not captured). This transformation is precisely the same as the one which is applied to other forms of `-> impl Trait`, except that it applies to an associated type and not a top-level type alias. +When desugaring, captured parameters from the method are reflected as generic parameters on the `$` associated type. Furthermore, the `$` associated type brings whatever where clauses are declared on the method into scope, excepting those which reference parameters that are not captured. This transformation is precisely the same as the one which is applied to other forms of `-> impl Trait`, except that it applies to an associated type and not a top-level type alias. -Exaample: +Example: ```rust trait RefIterator for Vec { @@ -265,6 +264,26 @@ impl Foo for u32 { Results in an impl in the `impl Foo for u32`. The [Future possibilities section](#future-possibilities) discusses some possible ways we could mitigate this in the future by other language extensions. +## Does using `-> impl Trait` in a trait limit users of that trait? + +If you only consider the mechanisms specified in this trait, then using `-> impl Trait` in a trait definition means that consumers of the trait cannot name the resulting return type. [As @Gankra highlighted in a comment on this RFC](https://github.com/rust-lang/rfcs/pull/3193#issuecomment-965505149), this limits reuse. For example, this RFC discusses a `NewIntoIterator` trait. Using this trait, one cannot write a signature like the following, because there is no `IntoIter` associated type: + +```rust +fn is_palindrome(iterable: Iter) -> bool +where + Iter: IntoIterator, + Iter::IntoIter: DoubleEndedIterator, + T: Eq; +``` + +The same problem applies to async functions in traits, which sometimes wish to be able to [add `Send` bounds to the resulting futures](https://rust-lang.github.io/async-fundamentals-initiative/evaluation/challenges/bounding_futures.html). + +## Do you have plans to make it possible to name the return types, then, and lift that limitation? + +Funny you should ask! If you check out the [future possibilities](#future-possibilities) section, you will see discussion of adding a mechanism to support naming the type returned by an impl trait. We believe that one could use this mechanism to overcome the limitations described in the previous question. + +## Can we make + # Prior art [prior-art]: #prior-art @@ -282,4 +301,4 @@ We expect to introduce a mechanism for naming the result of `-> impl Trait` retu Similarly, we expect to be introducing language extensions to address the inability to use `-> impl Trait` types with dynamic dispatch. These mechanisms are needed for async fn as well. A good writeup of the challenges can be found on the "challenges" page of the [async fundamentals initiative](https://rust-lang.github.io/async-fundamentals-initiative/evaluation/challenges/dyn_traits.html). -Finally, it would be possible to introduce a mechanism that allows users to give a name to the associated type that is returned by impl trait. One proposed mechanim is to support an inference mechanism, so that one if you have a function `fn foo() -> Self::Foo` that returns an associated type, the impl only needs to implement the function, and the compiler infers the value of `Foo` from the return type. Another options would be to extend the impl trait syntax generally to let uses give a name to the type alias or parameter that is introduced. \ No newline at end of file +Finally, it would be possible to introduce a mechanism that allows users to give a name to the associated type that is returned by impl trait. One proposed mechanism is to support an inference mechanism, so that one if you have a function `fn foo() -> Self::Foo` that returns an associated type, the impl only needs to implement the function, and the compiler infers the value of `Foo` from the return type. Another options would be to extend the impl trait syntax generally to let uses give a name to the type alias or parameter that is introduced. \ No newline at end of file From a50c547ec80d38522057b802f1b2752114f0c0c4 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Tue, 16 Nov 2021 13:05:15 -0500 Subject: [PATCH 3/3] edit --- ...00-return-position-impl-trait-in-traits.md | 120 ++++++++++-------- 1 file changed, 69 insertions(+), 51 deletions(-) diff --git a/text/0000-return-position-impl-trait-in-traits.md b/text/0000-return-position-impl-trait-in-traits.md index 96967862929..d4ea1334b40 100644 --- a/text/0000-return-position-impl-trait-in-traits.md +++ b/text/0000-return-position-impl-trait-in-traits.md @@ -197,6 +197,75 @@ trait RefIterator for Vec { To start, traits that use `-> impl Trait` will not be considered dyn safe, *even if the method has a `where Self: Sized` bound*. This is because dyn types currently require that all associated types are named, and the `$` type cannot be named. The other reason is that the value of `impl Trait` is often a type that is unique to a specific impl, so even if the `$` type *could* be named, specifying its value would defeat the purpose of the `dyn` type, since it would effectively identify the dynamic type. +# Drawbacks +[drawbacks]: #drawbacks + +This section discusses known drawbacks of the proposal as presently designed and (where applicable) plans for mitigating them in the future. + +## Cannot migrate off of impl Trait + +In this RFC, if you use `-> impl Trait` in a trait definition, you cannot "migrate away" from that. In other words, we cannot evolve: + +```rust +trait NewIntoIterator { + type Item; + fn into_iter(self) -> impl Iterator; +} +``` + +into + +```rust +trait NewIntoIterator { + type Item; + type IntoIter: Iterator; + fn into_iter(self) -> Self::IntoIter; +} +``` + +without breaking semver compatibility. The [future possibilities](#future-possibilities) section discusses one way to resolve this, by permitting impls to elide the definition of associated types whose values can be inferred from a function return type. + +## Clients of the trait cannot name the resulting associated type, limiting extensibility + +[As @Gankra highlighted in a comment on this RFC][gankra], the traditional `IntoIterator` trait permits clients of the trait to name the resulting iterator type and apply additional bounds: + +[gankra]: https://github.com/rust-lang/rfcs/pull/3193#issuecomment-965505149 + +```rust +fn is_palindrome(iterable: Iter) -> bool +where + Iter: IntoIterator, + Iter::IntoIter: DoubleEndedIterator, + T: Eq; +``` + +The `NewIntoIterator` trait used as an example in this RFC, however, doesn't support this kind of usage, because there is no way for users to name the `IntoIter` type (and, as discussed in the previous section, there is no way for users to migrate to a named associated type, either!). The same problem applies to async functions in traits, which sometimes wish to be able to [add `Send` bounds to the resulting futures](https://rust-lang.github.io/async-fundamentals-initiative/evaluation/challenges/bounding_futures.html). + +The [future possibilities](#future-possibilities) section discusses a planned extension to support naming the type returned by an impl trait, which could work to overcome this limitation for clients. + +## Impls cannot add new methods to the resulting types + +Similarly to the previous point, [@Gankra also pointed out][gankra] that impls which employ `-> impl Trait` are not able to add methods or implement traits on the resulting types. With `IntoIterator`, one has the option of adding inherent (or trait) methods to the resulting type: + +```rust +struct MyIterator { + ... +} + +impl IntoIterator for MyType { + type IntoIter = MyIterator; + ... +} + +impl MyIterator { + fn extra_method(&self) { + + } +} +``` + +Using `-> impl Trait` tends to make that not work. One way to address this would be to permit impls to specify the return types of `-> impl Trait` as concrete types. + # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives @@ -231,57 +300,6 @@ Yes, so long as the compiler has enough type information to figure out which imp [tait]: https://rust-lang.github.io/impl-trait-initiative/explainer/tait.html -## Would introducing a named associated type be a breaking change for a trait? - -Converting from returning impl trait to an explicit associated type is a breaking change for impls of the trait. Given this code: - -```rust -trait Foo { - fn bar() -> impl Display; -} - -impl Foo for u32 { - fn bar() -> impl Display { - self - } -} -``` - -transforming it to the following: - -```rust -trait Foo { - type Bar: Display; - fn bar() -> Self::Bar; -} - -impl Foo for u32 { - fn bar() -> impl Display { - self - } -} -``` - -Results in an impl in the `impl Foo for u32`. The [Future possibilities section](#future-possibilities) discusses some possible ways we could mitigate this in the future by other language extensions. - -## Does using `-> impl Trait` in a trait limit users of that trait? - -If you only consider the mechanisms specified in this trait, then using `-> impl Trait` in a trait definition means that consumers of the trait cannot name the resulting return type. [As @Gankra highlighted in a comment on this RFC](https://github.com/rust-lang/rfcs/pull/3193#issuecomment-965505149), this limits reuse. For example, this RFC discusses a `NewIntoIterator` trait. Using this trait, one cannot write a signature like the following, because there is no `IntoIter` associated type: - -```rust -fn is_palindrome(iterable: Iter) -> bool -where - Iter: IntoIterator, - Iter::IntoIter: DoubleEndedIterator, - T: Eq; -``` - -The same problem applies to async functions in traits, which sometimes wish to be able to [add `Send` bounds to the resulting futures](https://rust-lang.github.io/async-fundamentals-initiative/evaluation/challenges/bounding_futures.html). - -## Do you have plans to make it possible to name the return types, then, and lift that limitation? - -Funny you should ask! If you check out the [future possibilities](#future-possibilities) section, you will see discussion of adding a mechanism to support naming the type returned by an impl trait. We believe that one could use this mechanism to overcome the limitations described in the previous question. - ## Can we make # Prior art