- Feature Name: Trait alias
- Start Date: 2016-08-31
- RFC PR: rust-lang/rfcs#1733
- Rust Issue: rust-lang/rust#41517
Summary
Traits can be aliased with the trait TraitAlias = …;
construct. Currently, the right hand side is
a bound – a single trait, a combination with +
traits and lifetimes. Type parameters and
lifetimes can be added to the trait alias if needed.
Motivation
First motivation: impl
Sometimes, some traits are defined with parameters. For instance:
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.
// in the backend crate
pub struct Backend;
impl Foo<Backend> for i32 {
// ...
}
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 leave
the backend singleton type hidden in the crate. The first shot would be to create a new trait for
our backend:
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
:
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 explicitly 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:
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
.
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
Syntax
Declaration
The syntax chosen to declare a trait alias is:
trait TraitAlias = Trait;
Trait aliasing to combinations of traits is also provided with the standard +
construct:
trait DebugDefault = Debug + Default;
Optionally, if needed, one can provide a where
clause to express bounds:
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:
trait DebugDefault = where Self: Debug + Default;
It’s also possible to partially bind associated types of the right hand side:
trait IntoIntIterator = IntoIterator<Item=i32>;
This would leave IntoIntIterator
with a free parameter being IntoIter
, and it should be bind
the same way associated types are bound with regular traits:
fn foo<I>(int_iter: I) where I: IntoIntIterator<IntoIter = std::slice::Iter<i32>> {}
A trait alias can be parameterized over types and lifetimes, just like traits themselves:
trait LifetimeParametric<'a> = Iterator<Item=Cow<'a, [i32]>>;`
trait TypeParametric<T> = Iterator<Item=Cow<'static, [T]>>;
Specifically, the grammar being added is, in informal notation:
ATTRIBUTE* VISIBILITY? trait IDENTIFIER(<GENERIC_PARAMS>)? = GENERIC_BOUNDS (where PREDICATES)?;
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.
GENERIC_PARAMS
is a comma-separated list of zero or more lifetime and type parameters,
with optional bounds, just like other generic definitions.
Use semantics
You cannot directly impl
a trait alias, but you can have them as bounds, trait objects and
impl Trait.
It is an error to attempt to override a previously specified equivalence constraint with a non-equivalent type. For example:
trait SharableIterator = Iterator + Sync;
trait IntIterator = Iterator<Item=i32>;
fn quux1<T: SharableIterator<Item=f64>>(...) { ... } // ok
fn quux2<T: IntIterator<Item=i32>>(...) { ... } // ok (perhaps subject to lint warning)
fn quux3<T: IntIterator<Item=f64>>(...) { ... } // ERROR: `Item` already constrained
trait FloIterator = IntIterator<Item=f64>; // ERROR: `Item` already constrained
When using a trait alias as a trait object, it is subject to object safety restrictions after substituting the aliased traits. This means:
- 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); - all the associated types of the trait need to be specified;
- the
where
clause, if present, only contains bounds onSelf
.
Some examples:
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 is
fixed.
Ambiguous constraints
If there are multiple associated types with the same name in a trait alias, then it is a static error (“ambiguous associated type”) to attempt to constrain that associated type via the trait alias. For example:
trait Foo { type Assoc; }
trait Bar { type Assoc; } // same name!
// This works:
trait FooBar1 = Foo<Assoc = String> + Bar<Assoc = i32>;
// This does not work:
trait FooBar2 = Foo + Bar;
fn badness<T: FooBar2<Assoc = String>>() { } // ERROR: ambiguous associated type
// Here are ways to workaround the above error:
fn better1<T: FooBar2 + Foo<Assoc = String>>() { } // (leaves Bar::Assoc unconstrained)
fn better2<T: FooBar2 + Foo<Assoc = String> + Bar<Assoc = i32>>() { } // constrains both
Teaching
Traits 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 section)
- in an
impl Trait
Examples should be showed for all of the three cases above:
As a bound
trait StringIterator = Iterator<Item=String>;
fn iterate<SI>(si: SI) where SI: StringIterator {} // used as bound
As a trait object
fn iterate_object(si: &StringIterator) {} // used as trait object
In an impl Trait
fn string_iterator_debug() -> impl Debug + StringIterator {} // used in an impl Trait
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:
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
-
Adds another construct to the language.
-
The syntax
trait TraitAlias = Trait
requires lookahead in the parser to disambiguate a trait from a trait alias.
Alternatives
Should we use type
as the keyword instead of trait
?
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.
Supertraits & universal impl
It’s possible to create a new trait that derives the trait to alias, and provide a universal impl
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.
ConstraintKinds
Similar to GHC’s ConstraintKinds
, 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.
Syntax for sole where
clause.
The current RFC specifies that it is possible to use only the where
clause by leaving the list of traits empty:
trait DebugDefault = where Self: Debug + Default;
This is one of many syntaxes that are available for this construct. Alternatives include:
trait DebugDefault where Self: Debug + Default;
(which has been considered and discarded because it might look too much like a new trait definition)trait DebugDefault = _ where Self: Debug + Default;
(which was considered and then removed because it is technically unnecessary)trait DebugDefault = Self where Self: Debug + Default;
(analogous to previous case but not formally discussed)
Unresolved questions
Trait alias containing only lifetimes
This is annoying. Consider:
trait Static = 'static;
fn foo<T>(t: T) where T: Static {}
Such an alias is legit. However, I feel concerned about the actual meaning of the declaration – i.e.
using the trait
keyword to define alias on lifetimes seems a wrong design choice and seems not
very consistent.
If we chose another keyword, like constraint
, I feel less concerned and it would open further
opportunities – see the ConstraintKinds
alternative discussion above.
Which bounds need to be repeated when using a trait alias?
RFC 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 constraint
alternative sidesteps this issue.
What about bounds on type variable declaration in the trait alias?
trait Foo<T: Bar> = PartialEq<T>;
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:
trait Foo<T> = where Self: PartialEq<T>, T: Bar;
Issue 21903 explains the same problem for type aliasing.
Note: what about the following proposal below?
When using a trait alias as a bound, you cannot add extra bound on the input parameters, like in the following:
trait Foo<T: Bar> = PartialEq<T>;
Here, T
adds a Bar
bound. Now consider:
trait Bar<T> = PartialEq<T: Bar>;
Currently, we don’t have a proper understanding of that situation, because we’re adding in both
cases a bound, and we don’t know how to disambiguate between pre-condition and implication. That
is, is that added Bar
bound a constraint that T
must fulfil in order for the trait alias to be
met, or is it a constraint the trait alias itself adds? To disambiguate, consider:
trait BarPrecond<T> where T: Bar = PartialEq<T>;
trait BarImplic<T> = PartialEq<T> where T: Bar;
trait BarImpossible<T> where T: Bar = PartialEq<T> where T: Bar;
BarPrecond
would require the use-site code to fulfil the constraint, like the following:
fn foo<A, T>() where A: BarPrecond<T>, T: Bar {}
BarImplic
would give us T: Bar
:
fn foo<A, T>() where A: BarImplic<T> {
// T: Bar because given by BarImplic<T>
}
BarImpossible
wouldn’t compile because we try to express a pre-condition and an implication for
the same bound at the same time. However, it’d be possible to have both a pre-condition and an
implication on a parameter:
trait BarBoth<T> where T: Bar = PartialEq<T> where T: Debug;
fn foo<A, T>() where A: BarBoth<T>, T: Bar {
// T: Debug because given by BarBoth
}