Extend #![feature(trait_alias)] to permit impl blocks for most trait aliases.
Also support fully-qualified method call syntax with such aliases.
Additionally, allow trait aliases to have bodies, which can contain types,
consts, and/or fns.
Motivation
Often, one desires to have a "weak" version of a trait, as well as a "strong" one providing additional guarantees. Specifically, this RFC addresses trait relationships with the following properties:
- For any implementation of the "strong" variant, there is exactly one way to implement the "weak" variant.
- For any implementation of the "weak" variant, there is at most one way to implement the "strong" variant.
Subtrait relationships are commonly used to model this, but this often leads to coherence and backward compatibility issues.
In addition, sometimes one may wish to split a trait into two parts; however, this is impossible to accomplish backward-compatibly at present.
It is also impossible to rename trait items in a backward-compatible way.
AFIT Send bound aliases
Imagine a library, frob-lib, that provides a trait with an async method.
//! crate `frob-lib`
Most of frob-lib's users will need Frob::frob's return type to be Send, so
the library wants to make this common case as painless as possible. But
non-Send usage should be supported as well.
MVP: trait_variant
Because Return Type Notation isn't supported yet, frob-lib follows the
recommended practice of using the
trait-variant crate to have Send and
non-Send variants.
//! crate `frob-lib`
However, this API has limitations. Fox example, frob-lib may want to offer a
DoubleFrob wrapper:
;
As written, this wrapper only implements LocalFrob, which means that it's not
fully compatible with work-stealing executors. So frob-lib tries to add a
Frob implementation as well:
Coherence, however, rejects this.
error[E0119]: conflicting implementations of trait `LocalFrob` for type `DoubleFrob<_>`
--> src/lib.rs:1:1
|
1 | #[trait_variant::make(Frob: Send)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `DoubleFrob<_>`
...
8 | impl<T: LocalFrob> LocalFrob for DoubleFrob<T> {
| ---------------------------------------------- first implementation here
|
= note: this error originates in the attribute macro `trait_variant::make` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0119`.
With the trait_variant-based design, it's impossible to support both Send
and non-Send usage in the same DoubleFrob type.
Migrating to Return Type Notation
A few Rust releases later, Return Type Notation is stabilized. frob-lib wants
to migrate to it in order to address the issues with the trait_variant
solution:
//! crate `frob-lib``
// or whatever RTN syntax is decided on
However, this is an incompatible change; all implementations of Frob are
broken!
//! crate `downstream`
use Frob;
;
All impl blocks for Frob must be migrated to reference LocalFrob instead.
//! crate `downstream`
use LocalFrob;
;
Not only is this change disruptive, it also results in more confusing code.
downstream is written for work-stealing executors, but needs to reference
LocalFrob anyway.
With today's #![feature(trait_alias)]
What if frob-lib looked like this instead?
//! crate `frob-lib`
With today's trait_alias, it wouldn't make much difference for downstream.
impl blocks for Frob would still be broken.
Splitting a trait
Deref → Receiver + Deref
Niko Matsakis wants to split up the Deref
trait.
Deref currently looks like this:
Niko wants to split off the type Target part into a separate Receiver
supertrait. But there is no good backward-compatible way to do this at present.
Iterator → LendingIterator + Iterator
Once the necessary language features are stabilized, the library team will
likely want to add a LendingIterator trait to the standard library, that looks
like this:
Ideally, every Iterator should automatically be a LendingIterator. But,
again, there is no good way to do this right now.
Removing a Sized bound
Consider this humble library:
Currently, Frob::Frobber has a Sized bound, but the signature of frob()
doesn't require it. However, there is no backward-compatible way at present for
frob-lib to remove the bound.
Renaming trait items
Consider this verbose library:
The library author may want to rename the trait and its items to something less unwieldy. Unfortunately, he has no good way to accomplish this at present.
Guide-level explanation
impl blocks for trait aliases
With #![feature(trait_alias)] (RFC #1733), one can define trait aliases, for
use in bounds, trait objects, and impl Trait. This feature additionally allows
writing impl blocks for a subset of trait aliases.
Let's rewrite our AFIT example from before using this feature. Here's what it looks like now:
//! crate `frob-lib`
//! crate `downstream`
use Frob;
;
impls of Frob now Just Work.
Bodies for trait aliases
Trait aliases can also now specify an optional body, which can contain various items. These items are themselves aliases for items defined on the respective traits.
//! crate `foolib`
You can then refer to these associated items wherever the alias is in scope:
You can also use the alias names when implementing the trait alias:
This could be used to add LendingIterator as a supertrait of Iterator, as
mentioned in the motivation section:
You’ll note that Iterator’s body does not explicitly define Item. Instead,
it’s defined implicitly by the for<'a> <Self as LendingIterator>::Item<'a> = <Self as Iterator>::Item clause in the alias definition.
(N.B: In today’s Rust, the for<'a> LendingIterator<Item<'a> = … bound implies
'static, which is not desired in this case. This is a longstanding issue with
GATs which this RFC does not aim to address. The real Iterator alias may end
up looking slightly different, depending on how that issue is eventually
resolved.)
Implementing trait aliases for multiple traits
Trait aliases that combine multiple traits with + are also implementable:
However, be careful: if both traits have an item of the same name, you won’t be
able to disambiguate, and will have to split the impl block into separate
impls for the two underlying traits. Or, alternatively, you can give the trait
alias a body, and define item aliases with distinct names for each of the
conflicting items.
We can use this to split the Deref trait, as suggested in the motivation section:
//! New `Deref`
Reference-level explanation
Implementing trait aliases
A trait alias is considered implementable if it includes at least one trait
reference before the where keyword. (Henceforth, these are the “primary
traits” of the alias.)implementing the alias implements these primary traits,
and only these traits. The alias’s where clauses are enforced as requirements
that the impling type must meet—just like where clauses in trait
declarations are treated.
;
Bounds on generic parameters are also enforced at the impl site.
// Error: `*const i32` is not `Send`
If the trait alias uniquely constrains a portion of the impl block, that part
can be omitted.
Such constraints can be inferred indirectly:
Alias impls also allow omitting implied #[refine]s:
//! crate frob-lib
// not `+ Send`!
//! crate joes-crate
use Frob;
;
Trait aliases are unsafe to implement iff one or more primary traits are
marked unsafe.
Usage in paths
Trait aliases can also be used with trait-qualified and fully-qualified method
call syntax, as well as in paths more generally. When used this way, they are
treated equivalently to the underlying primary trait(s), with the additional
restriction that all where clauses and type parameter/associated type bounds
must be satisfied.
use array;
Implementable trait aliases can also be used with associated type bounds.
Items from traits in where clauses of the alias are accessible, unless
shadowed by items in the primary trait(s):
Aliases with multiple primary traits
A trait alias with multiple primary traits can be implemented, unless one of the primary traits requires specifying an item that conflicts with an item of the same name in a different primary trait.
// This isn't implementable, due to conflict between `Foo::frob` and `Bar::frob`
If the conflicting items all have defaults, the alias will be implementable, but overriding the defaults will not be possible.
// This is implementable, but the `impl` block won't be able
// to override the default bodies of the `frob()` functions.
Name conflicts of this sort also cause ambiguity when using the alias:
To resolve these conflicts, you can use trait alias bodies, as described below.
Bodies for trait aliases
Trait aliases can now optionally contain a body, which specifies various alias items. These can be types, constants, or functions. It must always be possible to derive the value of these items from the implementations of the aliased traits; compilation will fail otherwise.
types and const alias items in trait alias bodies
<T as Alias>::AssocVec means the same thing as Vec<<T as Foo>::Assoc>, and
<T as Alias>::ASSOC_PLUS_1 is equivalent to const { <T as Foo>::ASSOC + 1 }.
Alias items defined in a trait alias body shadow items of the same name in primary traits.
As aliases, type and const alias items neither require nor accept bounds or
where clauses; these are taken from the things being aliased. (This only
applies to the definitions of the items, not implementations of them.)
type and const alias items may appear in implementations of the alias:
Such implementations are permitted only if the compiler is able to verify that
they are consistent with the rest of the impl. Notably, the compiler can’t
invert nontrivial const expressions (more complicated than simply setting
equal to some other constant). For example, given the following trait and alias:
This doesn’t work:
However, this does:
But this doesn’t:
Alias items may be defined implicitly, through bounds on the trait alias
itself. For example, here is
TryFuture
as an implementable trait alias:
/// This means:
/// "A `TryFuture` is a `Future` where there exist
/// unique types `Self::Ok` and `Self::Error` such that
/// `Self: Future<Output = Result<Self::Ok, Self::Error>>`."
// Example impl
;
GATs in type alias bodies
Type alias bodies can also contain GAT alias items:
Bounds
fns in type alias bodies
Implementable fns
Trait alias bodies can also contain function alias items for methods of its primary trait(s). This involves a new syntax form for implementable function aliases:
Modifiers like const, async, unsafe, or extern "C" are neither required
nor accepted.
You are allowed to specify the generic parameters, in order to reorder them. But you don't have to:
Just like type alias items, implementable fn alias items neither require nor
accept where clauses or bounds of any sort.
In implementations, these methods look no different from any other:
Non-implementable fns
A trait alias body can also contain non-alias fns, with bodies. These are not
implementable:
This is similar to defining an extension trait like
Itertools.
(One difference from extension traits is that trait aliases do not create their
own dyn types.)
These non-alias function items can specify where clauses and bounds like any
other function item. They also have the same default Sized bounds on their
generic type parameters.
Interaction with dyn
Trait aliases do not define their own dyn types. This RFC does not change that
pre-existing behavior. However, we do make one change to which trait aliases
also define a type alias for a trait object. If a trait alias contains multiple
non-auto traits (primary or not), but one of them is a subtrait of all the
others, then the corresponding dyn type for that trait alias is now an alias
for the dyn type for that subtrait.
This is necessary to support the Deref example from earlier.
N.B.: when using implementable trait aliases to split a trait into two parts
without a supertrait/subtrait relationship between them, you have to be
careful in order to preserve dyn compatibility.
To make it work, you can do:
Drawbacks
- The fact that
trait Foo = Bar + Send;means something different thantrait Foo = Bar where Self: Send;will likely be surprising to many. - Adds complexity to the language. In particular, trait alias bodies introduce a large amount of new syntax and complexity, but will likely be rarely used.
- There is a lot of overlap between trait alias bodies and extension traits.
Rationale and alternatives
Allow subtrait implementations to include supertrait items directly
You’ll note, in the Deref/Receiver example, that we had to create a new
trait called DerefToTarget:
//! New `Deref`
If we “just” allowed implementations of any trait to also implement their supertraits, as has been proposed by others, this extra trait would not be necessary.
However, this RFC very deliberately does not propose that. The reason for this
is that a trait impl is more than just its items. This is most apparent
with marker traits: implementing a trait like Ord does not require defining
any items at all, but it imposes important restrictions on the implementing type
nevertheless. If those requirements are not upheld, all kinds of bugs could
occur. If the trait is unsafe, an erroneous implementation could even be
unsound!
Because of these risks, when a code reviewer encounters a new trait impl
block, they should be able to tell, from just the block’s header and the
definition of the trait it names, what new traits are being implemented, and
therefore what new invariants must be upheld for those implementations to be
valid. If subtrait impl blocks could silently also implement supertraits, that
would no longer be possible.
Require an attribute to make aliases implementable
We could require an attribute on implementable aliases; e.g. #[implementable] trait Foo = .... However, there is not much reason to opt out of
implementability. And if the alias definer really wants to make their alias
inimplementable, they can simply no include any primary traits (make all traits
secondary).
Don’t have trait alias bodies
Not including this part of the proposal would significantly decrease the overall complexity of the feature. However, it would also reduce its power: trait aliases could no longer be used to rename trait items, and naming conflicts in multi-primary-trait aliases would be impossible to resolve.
It's this last issue especially that leads me to not relegate this to a future
possibility. Adding a defaulted item to a trait should at most require minor
changes to dependents, and restructuring a large impl block is not “minor”.
Don’t have non-implementable fns` in trait alias bodies
Such items don't have much utility for preserving backward compatibility, and overlap with extension traits. However, the cost of allowing them is low. This RFC is deliberately written to be as expansive as possible, so I chose to include them.
Constrain generic parameters
A previous version of this RFC required generic parameters of implementable trait aliases to be used as generic parameters of a primary trait of the alias. This restriction was meant to avoid surprising errors:
// ERROR: `T`` is unconstrained
However, upon further discussion, I now lean toward allowing more flexibility, even at the risk of potential confusion.
Allow impl Foo + Bar for Type { ... } directly, without an alias
It's a forward-compatibility hazard (if the traits gain items with conflicting names), with no use-case that I can see.
Allow implementing aliases with 0 primary traits
We could allow implementing aliases with no primary traits, as a no-op. However, not allowing this enables users deliberately force an alias to be non-implementable, which is far more useful (e.g., if they don’t want to commit to which traits to make primary vs secondary).
Prior art
Unresolved questions
- How does
rustdocrender these?
Future possibilities
- We could allow trait aliases to define their own defaults for
impls. One possibility is thedefault partial implsyntax I suggested on IRLO. default partial implwould also address the case where one wants to split a trait in two, but the supertrait has methods with default impls in terms of the subtrait.- We could allow implementable
fnaliases in non-aliastraitdefinitions. - We could add an attribute for trait aliases to opt in to generating their own
dyntype.- This could be prototyped as a proc macro.
New kinds of bounds
Anything that makes where clauses more powerful would make this feature more
powerful as well.
For example, if we could write bounds for the constness of a method, that
could allow emulating some of const
traits—or even form part of the
desugaring for that feature:
Additionally, if we could write bounds for the target features required by a function, that could also be leveraged by implementable aliases:
Additional language extensions might be necessary to handle cases where the set of target features a trait implementation may or may not use is large or open: