-
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
Trait alias. #1733
Trait alias. #1733
Changes from 10 commits
55de6a3
c0c4834
cf74fd5
042b8e4
88d3074
5d0b4fe
819d8f9
8a72471
554c4c1
7b63d30
6601146
b1be75b
90ce5c3
f04d4b2
f269f9a
3fc5568
8182509
83322de
310c7fe
eb24a49
4d99e23
54ca931
bf3414d
80dbc13
a2f8020
dcec407
b5ff949
2b14c14
5c94f49
2a1a5b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
- Feature Name: Trait alias | ||
- Start Date: 2016-08-31 | ||
- RFC PR: | ||
- Rust Issue: | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
Traits can be aliased the same way types can be aliased with the `type` keyword. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
## First motivation: `impl` | ||
|
||
Sometimes, some traits are defined with parameters. For instance: | ||
|
||
```rust | ||
pub trait Foo<T> { | ||
// ... | ||
} | ||
``` | ||
|
||
It’s not uncommon to do that in *generic* crates and implement them in *backend* crates, where the | ||
`T` template parameter gets substituted with a *backend* type. | ||
|
||
```rust | ||
// in the backend crate | ||
pub struct Backend; | ||
|
||
impl trait Foo<Backend> for i32 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a typo, right? That is, It should just be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
// ... | ||
} | ||
``` | ||
|
||
Users who want to use that crate will have to export both the trait `Foo` from the generic crate | ||
*and* the backend singleton type from the backend crate. Instead, we would like to be able to let | ||
the backend singleton type hidden in the crate. The first shot would be to create a new trait for | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand the english here. "We would like to be able to let the backend singleton type hidden in the crate" ??? Is there a wrong word or a missing verb somewhere there? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I should have used “keep” instead of “let”? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, that would work. Or "leave" instead of "let" would also work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
our backend: | ||
|
||
```rust | ||
pub trait FooBackend: Foo<Backend> { | ||
// ... | ||
} | ||
|
||
fn use_foo<A>(_: A) where A: FooBackend {} | ||
``` | ||
|
||
If you try to pass an object that implements `Foo<Backend>`, that won’t work, because it doesn’t | ||
implement `FooBackend`. However, we can make it work with the following universal `impl`: | ||
|
||
```rust | ||
impl<T> FooBackend for T where T: Foo<Backend> {} | ||
``` | ||
|
||
With that, it’s now possible to pass an object that implements `Foo<Backend>` to a function | ||
expecting a `FooBackend`. However, what about impl blocks? What happens if we implement only | ||
`FooBackend`? Well, we cannot, because the trait explicitely states that we need to implement | ||
`Foo<Backend>`. We hit a problem here. The problem is that even though there’s a compatibility at | ||
the `trait bound` level between `Foo<Backend>` and `FooBackend`, there’s none at the `impl` level, | ||
so all we’re left with is implementing `Foo<Backend>` – that will also provide an implementation for | ||
`FooBackend` because of the universal implementation just above. | ||
|
||
## Second example: ergonomic collections and scrapping boilerplate | ||
|
||
Another example is associated types. Take the following [trait from tokio](https://docs.rs/tokio-service/0.1.0/tokio_service/trait.Service.html): | ||
|
||
```rust | ||
pub trait Service { | ||
type Request; | ||
type Response; | ||
type Error; | ||
type Future: Future<Item=Self::Response, Error=Self::Error>; | ||
fn call(&self, req: Self::Request) -> Self::Future; | ||
} | ||
``` | ||
|
||
It would be nice to be able to create a few aliases to remove boilerplate for very common | ||
combinations of associated types with `Service`. | ||
|
||
```rust | ||
Service<Request = http::Request, Response = http::Response, Error = http::Error>; | ||
``` | ||
|
||
The trait above is a http service trait which only the associated type `Future` is left to be | ||
implemented. Such an alias would be very appealing because it would remove copying the whole | ||
`Service` trait into use sites – trait bounds, or even trait impls. Scrapping such an annoying | ||
boilerplate is a definitive plus to the language and might be one of the most interesting use case. | ||
|
||
# Detailed design | ||
[design]: #detailed-design | ||
|
||
## Syntax | ||
|
||
The syntax chosen to make a *trait alias* is: | ||
|
||
```rust | ||
trait TraitAlias = Trait; | ||
``` | ||
|
||
Trait aliasing to combinations of traits is also provided with the standard `+` construct: | ||
|
||
```rust | ||
trait DebugDefault = Debug + Default; | ||
``` | ||
|
||
Optionally, if needed, one can provide a `where` clause to express *bounds*: | ||
|
||
```rust | ||
trait DebugDefault = Debug where Self: Default; // same as the example above | ||
``` | ||
|
||
Furthermore, it’s possible to use only the `where` clause by leaving the list of traits empty: | ||
|
||
```rust | ||
trait DebugDefault = where Self: Debug + Default; | ||
``` | ||
|
||
Specifically, the grammar being added is, in informal notation: | ||
|
||
``` | ||
ATTRIBUTE* VISIBILITY? trait IDENTIFIER(<GENERIC_PARAMS>)? = GENERIC_BOUNDS (where PREDICATES)?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I read the whole Syntax section, and failed to see explicit mention of support for the I infer that you want to support this syntax from the Motivation section of the RFC, specifically from your second example with the (If I recall correctly, Rust's current
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
``` | ||
|
||
`GENERIC_BOUNDS` is a list of zero or more traits and lifetimes separated by `+`, the same as the current syntax for bounds on a type parameter, and `PREDICATES` is a comma-separated list of zero or more predicates, just like any other `where` clause. A trait alias containing only lifetimes (`trait Static = 'static;`) is not allowed. | ||
|
||
## Semantics | ||
|
||
Trait aliases can be used in any place arbitrary bounds would be syntactically legal. | ||
|
||
You cannot directly `impl` a trait alias, but can have them as *bounds*, *trait objects* and *impl Trait*. | ||
|
||
When using a trait alias as an object type, it is subject to object safety restrictions _after_ substituting the aliased traits. This means: | ||
|
||
1. It contains an object safe trait, optionally a lifetime, and zero or more of these other bounds: `Send`, `Sync` (that is, `trait Show = Display + Debug;` would not be object safe). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wrote in hadronized#2 (comment):
The former is the conservative approach, the latter would be compatible with today's |
||
2. All the associated types of the trait need to be specified. | ||
3. The `where` clause, if present, only contains bounds on `Self`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wrote in hadronized#2 (comment):
|
||
|
||
Some examples: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume your system is meant to handle the case where a trait alias has specified some of the associated types, and you instantiate that alias supplying others. I would like examples that explicitly points out how such cases are handled. E.g. give us a trait defintion with two associated types, an alias that defines one of them, and then various uses of the alias. In particular:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (its possible that you actually covered some of this material in your further discussion of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I would not expect this to work, actually. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do we mean by 'instantiate'? If it means implement, we don't allow implementing aliases under this RFC. If it means something like this: trait Foo {
type A1;
type A2;
}
trait Bar = Foo<A1 = ()>;
trait Baz = Bar<A2 = ()>; I have not formed an opinion about whether that should work or not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @withoutboats I think it means something like this: trait Foo {
type A1;
type A2;
}
trait Bar = Foo<A1 = ()>;
// later, use `Bar<A2 = i32>` or just `Bar` if you don't need to constrain `Foo::A2` in your bound There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @solson yes I meant an example like your first one, i.e.: trait Foo { type A1; type A2; }
trait Bar = Foo<A1 = ()>; // is it okay to specify some associated types here ...
fn quux<T: Bar<A2=i32>(t: T) { ... } // ... and others here?
fn hoho<T: Bar<A1=f64, A2=()>>(t: T) { ... } // furthermore, can I revise the choice? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A related question: You show some trait aliases where the RHS supplies inputs for the trait parameters. And you show others where the trait parameter inputs are not supplied.
Do you want to support supplying only a prefix of the inputs? (I assume the answer is no, at least for this first version of the feature, but this should be made explicit.) trait Inputs<X, Y> { } // I have two inputs
trait Alias1 = Inputs; // This is legal by the current RFC (update: or maybe its not?)
trait Alias2 = Inputs<i32, i32>; // and this is also legal
trait Alias3 = Inputs<i32>; // but I assume this is not legal ...
trait Alias4<X> = Input<i32, X>; // ... and should instead be explicitly encoded like this (right?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @withoutboats I think there is a variant of the example you wrote down (where a series of trait aliases incrementally adds more constraints on the type parameters) actually already present in the RFC, down in the (All the more reason that this detail of the feature needs to be spelled out explicitly up here...) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it makes sense to implement equality constraints in full generality before this is stabilized? That might allow this to be more conservative while keeping full expressive power. I'm definitely against missing arguments being hacked to work. I'm also against (though less vehemently) allowing "equality constraint arguments" to aliases for now—just like impls of aliases, it's an idea that doesn't scale from single traits to arbitrary bounds.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A trait alias which partially applies the associated types of its supertrait(s) seems useful. Allowing a user of the trait alias to change those applications does not appeal to me, because then the alias definition is anti-self-documenting. You should make a separate alias without the associated types filled in if you intend it to be used that way. |
||
|
||
```rust | ||
trait Sink = Sync; | ||
trait ShareableIterator = Iterator + Sync; | ||
trait PrintableIterator = Iterator<Item=i32> + Display; | ||
trait IntIterator = Iterator<Item=i32>; | ||
|
||
fn foo1<T: ShareableIterator>(...) { ... } // ok | ||
fn foo2<T: ShareableIterator<Item=i32>>(...) { ... } // ok | ||
fn bar1(x: Box<ShareableIterator>) { ... } // ERROR: associated type not specified | ||
fn bar2(x: Box<ShareableIterator<Item=i32>>) { ... } // ok | ||
fn bar3(x: Box<PrintableIterator>) { ... } // ERROR: too many traits (*) | ||
fn bar4(x: Box<IntIterator + Sink + 'static>) { ... } // ok (*) | ||
``` | ||
|
||
The lines marked with `(*)` assume that [#24010](https://github.com/rust-lang/rust/issues/24010) is fixed. | ||
|
||
# Teaching | ||
[teaching]: #teaching | ||
|
||
[Traits](https://doc.rust-lang.org/book/traits.html) are obviously a huge prerequisite. Traits aliases could be introduced at the end of | ||
that chapter. | ||
|
||
Conceptually, a *trait alias* is a syntax shortcut used to reason about one or more trait(s). Inherently, the *trait alias* is usable | ||
in a limited set of places: | ||
|
||
- as a *bound*: exactly like a *trait*, a *trait alias* can be used to constraint a type (type parameters list, where-clause) | ||
- as a *trait object*: same thing as with a *trait*, a *trait alias* can be used as a *trait object* if it fits object safety restrictions (see above in the [semantics](#semantics) section) | ||
- in an [`impl Trait`](https://github.com/rust-lang/rfcs/blob/master/text/1522-conservative-impl-trait.md) | ||
|
||
Examples should be showed for all of the three cases above: | ||
|
||
#### As a bound | ||
|
||
```rust | ||
trait StringIterator = Iterator<Item=String>; | ||
|
||
fn iterate<SI>(si: SI) where SI: StringIterator {} // used as bound | ||
``` | ||
|
||
#### As a trait object | ||
|
||
```rust | ||
fn iterate_object(si: &StringIterator) {} // used as trait object | ||
``` | ||
|
||
#### In an `impl Trait` | ||
|
||
```rust | ||
fn string_iterator_debug() -> impl Debug + Iterator<Item=String> {} // used in an impl Trait | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one is not like the others? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
``` | ||
|
||
As shown above, a *trait alias* can substitute associated types. It doesn’t have to substitute them all. In that case, the *trait alias* | ||
is left incomplete and you have to pass it the associated types that are left. Example with the | ||
[tokio case]([tokio example](#second-example-ergonomic-collections-and-scrapping-boilerplate): | ||
|
||
```rust | ||
pub trait Service { | ||
type Request; | ||
type Response; | ||
type Error; | ||
type Future: Future<Item=Self::Response, Error=Self::Error>; | ||
fn call(&self, req: Self::Request) -> Self::Future; | ||
} | ||
|
||
trait HttpService = Service<Request = http::Request, Response = http::Response, Error = http::Error>; | ||
|
||
trait MyHttpService = HttpService<Future = MyFuture>; // assume MyFuture exists and fulfills the rules to be used in here | ||
``` | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
- Adds another construct to the language. | ||
|
||
- The syntax `trait TraitAlias = Trait` requires lookahead in the parser to disambiguate a trait from a trait alias. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ultimately, this will depend on what kind of "parser generation" strategy you are using, but there isn't an inherent need for lookahead here. The key point is that the same syntax one can use on a trait, one can use on a trait alias, right up until you find either an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, so we just remove that comment from the RFC? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think so |
||
|
||
# Alternatives | ||
[alternatives]: #alternatives | ||
|
||
- It’s possible to create a new trait that derives the trait to alias, and provide a universal `impl`: | ||
|
||
```rust | ||
trait Foo {} | ||
|
||
trait FooFakeAlias: Foo {} | ||
|
||
impl<T> Foo for T where T: FooFakeAlias {} | ||
``` | ||
|
||
This works for trait objects and trait bounds only. You cannot implement `FooFakeAlias` directly | ||
because you need to implement `Foo` first – hence, you don’t really need `FooFakeAlias` if you can | ||
implement `Foo`. | ||
|
||
There’s currently no alternative to the impl problem described here. | ||
|
||
- Similar to Haskell's ContraintKinds, we could declare an entire predicate as a reified list of constraints, instead of creating an alias for a set of supertraits and predicates. Syntax would be something like `constraint Foo<T> = T: Bar, Vec<T>: Baz;`, used as `fn quux<T>(...) where Foo<T> { ... }` (i.e. direct substitution). Trait object usage is unclear. | ||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
- Should we use `type` as the keyword instead of `trait`? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I would prefer to have this listed as an alternative but not an unresolved question, in that I don't expect us to revisit this question during stabilization. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider it done. |
||
|
||
`type Foo = Bar;` already creates an alias `Foo` that can be used as a trait object. | ||
|
||
If we used `type` for the keyword, this would imply that `Foo` could also be used as a bound as well. If we use `trait` as proposed in the body of the RFC, then `type Foo = Bar;` and `trait Foo = Bar;` _both_ create an alias for the object type, but only the latter creates an alias that can be used as a bound, which is a confusing bit of redundancy. | ||
|
||
However, this mixes the concepts of types and traits, which are different, and allows nonsense like `type Foo = Rc<i32> + f32;` to parse. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually...would expect that to parse, but fail in semantical resolution, since It's probably worth noting that this also interacts with the current way that we "pun" on a trait-name for the object type. If we adopted some keyword for objects, like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this does parse today (but then fails at a later stage). |
||
|
||
- Which bounds need to be repeated when using a trait alias? | ||
|
||
[RFC 1927](https://github.com/rust-lang/rfcs/pull/1927) intends to change the rules here for traits, and we likely want to have the rules for trait aliases be the same to avoid confusion. | ||
|
||
The `contraint` alternative sidesteps this issue. | ||
|
||
- What about bounds on type variable declaration in the trait alias? Consider the following: | ||
|
||
```rust | ||
trait Foo<T: Bar> = PartialEq<T>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is related to In other words, if we applied the same intuition here, but declared that unused bounds are illegal (in types we may have to permit them because back compat):
At minimum, we should cite issue rust-lang/rust#21903 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Issue cited, but I don’t get your first paragraph. In what scenario should we avoid issuing a warning? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, sorry, it's a bit confusing. What I am saying is that sometimes you need bounds, specifically when associated types are involved:
but other times, as in the example you gave, you are just creating "extra" bounds: trait Foo<T: Iterator> = PartialEq<T> I guess that for trait aliases we could interpret But presuming that we did not interpret it that way, and we wanted to make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a very interesting commment @Ericson2314 and I was thinking about it. I’m half editing the RFC talking about that. |
||
``` | ||
|
||
`PartialEq` has no super-trait `Bar`, but we’re adding one via our trait alias. What is the behavior | ||
of such a feature? One possible desugaring is: | ||
|
||
```rust | ||
trait Foo<T> = where Self: PartialEq<T>, T: Bar; | ||
``` | ||
|
||
- Do we really want hidden free type variables? | ||
|
||
Consider the following: | ||
|
||
```rust | ||
trait HttpService = Service<Request = http::Request, Response = http::Response, Error = http::Error>; | ||
``` | ||
|
||
This trait has a hidden `Future` associated type that is left as free type variable. In order to use that `HttpService` trait, we need | ||
to provide the `Future` type. Even though it’s always a good feature to have free associated types at the use site, I think it’s not | ||
at the declaration site, and I’d be more willing to have the following syntax – which seems sounder and is more explicit of the interface of the trait alias because it requires to implement all associated types: | ||
|
||
```rust | ||
trait HttpService<F> = Service<Request = http::Request, Response = http::Response, Error = http::Error, Future = F>; | ||
``` | ||
|
||
Having to implement all associated types – with concrete types or type variables – is less confusing when we look at the definition of trait alias in my opinion. |
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.
Is this summary still accurate? I think it should be s/
type
keyword/trait
keyword/, no?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 initial intention was to state that we can have alias the same way we already have with the
type
keyword and types. But you’re right, it’s confusing. I’m updating that.