We would like to allow nested trait impl blocks in trait defintion blocks and impl Trait for blocks, so that users can supply supertrait items in subtrait contexts.
Motivation
Trait evolution
Trait evolution is a treatment to existing trait hierarchy in a library. Difficulty has arose in the past that hoisting items from the current trait into a new supertrait, or introduction of a second trait.
This RFC promises to improve the situation around trait evolution. It captures the common cases under this theme and aims to reduce rewrites in downstream crates, should the need to re-organise trait hierarchy arises.
Example: trait refinement by forming trait hierarchy
One long-standing issue with std::fmt::{Read, Write} traits is that their method signatures involve a type std::fmt::Error that is only applicable to std environment. There has been growing interest in making these traits available in the #[no_std] environment.
The most promising refactoring could be introduction of a #[no_std] counterpart of the Write trait as an example. In doing so, the Error type could be specified with a custom error type that is suitable in the context.
The original trait would need to be reworked and ideally in a way that does not involve a massive ecosystem-wide rewrite by fixing the Error type to std::fmt::Error and absolving the requirement on existing downstream implementations to supply the same supertrait implementation all over again.
// In `std` crate ...
The same argument applies to std::io::{Read, Write} traits as well.
Example: trait refinement by item hoisting into supertraits
As library code grows, there is frequently a need to break down a big trait into several smaller traits. This would have been a breaking change in view of SemVer and a user code rewrite is mandatory. However, the aim of this RFC is to ease the transition of downstream trait implementors to the new trait hierarchy by reducing or eliminating the rewrites.
Suppose that we start with a big trait Subtrait and it becomes desirable that candidate_for_hoisting method is hoisted into another trait.
// downstream crate
With this RFC, it is possible for the library author to perform the following refactor.
Example: relaxed bounds via new supertraits
A common use case of supertraits is weaken bounds involved in associated items. There are occasions that a weakened supertrait could be useful. Suppose that we have a factory trait in the following example. In this example, the async fn make factory method could be weakened so that the future returned could be used in the context where the future is not required to be of Send. This has been enabled through the use of the trait_variant crate. The tower::Service trait would benefit greatly from this proposal by having also the !Send bound for local service without major refactoring.
// `trait_variant` will generate a conceptual subtrait:
This RFC enables one to construct the trait in the following fashion.
Example: automatic supertrait implementation
A second prominent example is the PartialOrd and Ord traits.
// This is one of the more probably implementation:
The PartialOrd trait could be reworked in this proposal as follows.
There are now two choices for type X in the downstream crate.
- Delete the
impl PartialOrd for X. Without the overlappingimpl, theauto implcan stand in and take effect.
// delete: impl PartialOrd for X { .. }
- Declare use of the existing applicable
impl PartialOrd for X.
Example: A possible ToOwned refactor
As of writing, ToOwned is defined as follows.
ToOwned is a trait that could be further refined into the AsOwned concept and itself.
The appeal of this refinement is that there is a proper separation between the capability to borrow out data and that to take and own data. This enables Cow<'_, _> to be implemented in the following snippet.
// Note that we do not out right require that the data can be taken and owned ...
Helpers for implementing traits
This is not the main motivation for this RFC, but it is a secondary additional feature that this RFC enables.
For some traits, it's difficult to implement the trait directly because the "raw" interface that the trait exposes is complex. If the trait is unsafe, this may be even worse as the end-user may not wish to write any unsafe code. This feature makes it easy to provide utilities for more easily implementing the trait.
Example: Serde
The serde traits are notoriously difficult to implement directly. It's almost always done by macro. Imagine if you could write this:
// For reference, the Serialize trait is taken from a recent version of the serde crate.
And then MyStruct automatically implements Serialize by creating a MyStructProxy instance and serializing the proxy. So for example MyStruct { name: "a", int: 42 } is serialized into json as {"name":"a","tens":4,"digit":2}.
Right now, the only way to provide a helper like the one above is to either:
- Implement a proxy that emits the
Serializeimpl block, or - Provide helper methods and instruct the user how to manually implement
Serializeusing the helpers you provided.
Hypothetical example: Evolving the `Deref` trait
This section is included because whether Deref is to be merged with Receiver is up for deliberation at the moment.
As part of the arbitrary_self_types feature, we need to split Deref into two traits. Right now, the Deref trait looks like this:
But we need it to look like this:
However, making this change is difficult due to backwards compatibility. There are many crates in the ecosystem with code that looks like this:
;
or like this:
The feature in this RFC provides a mechanism for trait evolution of Deref and Receiver where it becomes possible to split a trait into a super-trait and sub-trait without breaking backwards compatibility in downstream crates.
Why not a blanket impl?
Unfortunately, this does not work:
The problem is that crates need to be able to write this code:
;
This kind of code where SmartPtr sometimes implements Deref but always implements Receiver is not possible with a blanket implementation. The feature proposed by this RFC would make the above construction a possibility.
Tenets
- Backward-compatibility
- We need to maintain that all the current
implblocks to compile, specifically theimpl Dereflitmus test mentioned in the deref-receiver motivating example. - This demand also extends to other possible library traits that may see relocation of items into a future supertrait, while ensuring that the existing
implblocks continue to compile.
- We need to maintain that all the current
- Intuitive readability and clear syntatical signal
- We strive for a syntax that is Intuitive and easily connected to the existing constructs.
- A successful design is one that enables a user to easily build a correct mental picture of the trait
impls with assistance from the syntatical features. - By extension, supertrait
impls should appear clearly in connection with subtraitimpls.
- Flexibility
- We strive for providing users the means to refactor their traits without compromising the expressiveness of the trait relationships.
Guide-level explanation
This section gives an overview of the auto impl feature and some example use-cases.
Overview
It is possible to declare that implementations of a sub-trait should automatically implement a given supertrait.
Given the above traits, when you implement MySubTrait, you must also specify items from MyTrait.
In the above case, it is an error to not specify all items from MyTrait. However, it is possible to opt-out of implementing MyTrait in the impl MySubTrait block:
The extern impl MyTrait declaration specifies the impl block does not automatically implement MyTrait, and that another impl block is used instead.
Example: Trait evolution
Over time, needs have arose to establish a hierarchy of traits, so that the parts and pieces of existing "big" library traits, be it from std or ecosystem crates, can be extracted and pulled back into supertraits without requiring a breaking change in the downstream crates. In most cases, the association of methods, that are to be "refactored", and the destination supertraits can be determined without ambiguity. This falls under a bigger theme of trait evolution, which concerns how a historically big trait can be broken down and refined into smaller traits and trait hierarchy. RFC 1210 provided an example of a trait evolution and how specialisation could have eased the refactoring.
With this proposal, specialisation is not required and, instead, the pulled-back supertrait implementation applies directly within the context of the subtrait implementation.
// A refactored Subtrait that encodes parts of its protocol
// to be implementable on other types,
// to which Subtrait is not applicable.
// Subtrait is the refinement of the Supertrait protocol
// The proposal called for enabling specialisation of
// the following blanket implementation as *stop-gap*.
// We propose that for trait evolution, we do not need to rely on specialisation...
/**
impl<T: Subtrait> Supertrait for T {
default fn supertrait_fn() {
<T as Subtrait>::supertrait_fn();
}
}
**/
;
// ... but rather keep the current and future `impl` block
// the same,
Default auto implementations
It is possible to provide default implementations of functions from the super trait.
In this case, an impl block for MySubTrait will still automatically implement MyTrait, but you are not required to provide an implementation of my_func since a default implementation exists.
Example: Helpers for implementing a trait
With default auto implementations, it becomes possible to provide a sub-trait whose purpose is to help you implement the super trait in a specific way.
For example, given this super trait:
Then you might have a helper for implementing EventHandler for a specific event type:
This allows crates to implement EventHandler by writing this code:
;
The MouseEventHandler trait could even come from a different crate than EventHandler.
Unsafe auto impl
When the supertrait is an unsafe trait, its auto implementation must also be unsafe.
unsafe
This implies that it is unsafe to override the auto implementation.
If the super trait is unsafe, then the auto impl must also be unsafe.
Example: Safely implement unsafe trait
This can be used to provide a safe way to implement an unsafe trait.
/// Implementers must ensure that even() returns an
/// even number.
unsafe
// provides an impl for Even safely
Reference-level explanation
In a trait declaration, you may declare one or more auto impl items with a block that provides implementations for one or more items from the super trait.
All items inside the auto impl block must match an item from the super trait of the same name and signature. When the block is empty, it is legal to use a semicolon instead. That is, these are equivalent:
auto impl SuperTrait {}auto impl SuperTrait;
The type of the super trait can be anything that matches the TypePath grammar and evaluates to a trait. This means that, for example, this is legal:
The trait must be a super-trait. That is, the trait solver must be able to prove that where T: SubTrait implies where T: SuperTrait.
Impl blocks
Impl blocks using auto implementations are simply a short-hand for multiple impl blocks with all of the consequences that implies.
When a trait has an auto impl entry, all impl blocks for the trait that do not use extern impl to opt-out of the auto implementation become equivalent to two impl blocks, one for the sub-trait and one for the super-trait. They are generated according to these rules:
- Both impl blocks have the exact same set of generic items and where clauses, except that in the super trait any generic parameters that are unused by the super trait's
auto implare omitted. - The equivalent impl block for the super trait contains the items in the
implblock that come from the super trait, plus any items specified in the block of theauto implif any. In case of duplicates, the item from theimplblock is preferred. - The equivalent impl block for the sub-trait contains any remaining items in the original
implblock, plus anextern impldeclaration.
So for example, given these traits:
then this impl block:
is short-hand for this:
// The original impl block MINUS the super-trait's methods
// PLUS an extern impl statement:
// PLUS an additional impl block for the super trait,
// constructed in the following manner:
Note that this implies that you must use extern impl to provide your own implementation of the super trait. If you don't, then by the above rule, the generated impl for the super trait would overlap with your custom implementation, which is illegal by the standard trait rules.
Extension: item aliasing in auto impl
To reduce possible verbosity, we can propose a future extension to auto impl default implementation block, in case supertrait items can be implemented as an alias to subtrait items.
When a subtrait associated method has the same signature as a supertrait associated method in terms of generics, has a set of where bounds that satisfies the supertrait item where bounds and compatible function signature, the auto impl default implementation can be simplified into a assignment statement, instead of a complete function body with a delegation call.
Extension: auto impl support for higher-kinded superbound
Today a higher-kinded superbound is allowed as a superbound as long as only lifetime parameters are used.
We propose to extend auto impl support to superbounds like this. In order to achieve this,
the auto impl item in traits and impl blocks are equipped with exclusively lifetime generic
parameters.
The usual no-shadowing rule applies when it comes to lifetime parameters.
Unsafe auto implementations
If the super trait is unsafe, then the auto impl declaration must also be unsafe.
When an auto impl is declared unsafe, then the choices are
- To opt-out, and you must write
unsafe extern impl. - If any methods from the
unsafe auto implblock are to be overridden, then theauto implblock must beunsafe.
Naming ambiguity
If the sub-trait defines an item of the same name as a required item in the super-trait, then an auto impl block for that supertrait will be implicated so that it can provide an implementation of that item from the supertrait.
In this scenario, any item in an impl block of the sub-trait using the ambiguous name will always be resolved to the item from the sub-trait. This means that the only way to override the item from the super trait is to use extern impl or an overriding auto impl block inside the sub-trait impl block.
If the sub-trait definition contains two auto impl directives and a sub-trait implementation has an item with a name that ...
- can be resolved to a required associated item in both of the
auto implsupertraits, - but cannot be resolved to an associated item in the sub-trait trait definition,
then, irrespective of the associated item kind, the item must also be rejected as ambiguity. Either an extern impl statement or an overriding auto impl block is required for supplying an alternative definition of this item for each relevant supertrait.
On the contrary, when the sub-trait definition defines an item name and an auto impl whose supertrait also defines an item with a matching name, this name appearing in all implementations of this sub-trait will always resolve to the item associated to the sub-trait.
Nesting auto impl in sub-trait defintion
Nesting auto impl is allowed in a sub-trait definition or implementor.
In a sub-trait definition site, only auto impls is ever allowed in any level of nesting. If a target supertrait has at least one associated item or auto impl directive, either the full list of associated items and full list of auto impls with concrete implementation are supplied as a default implementation at one nesting level, or the auto impl implementation is elided and the nesting terminates at this supertrait.
In a sub-trait implementor site, both auto impls and extern impls are allowed.
Mandatory extern impl declaration
For the following definition, a non-marker trait is a trait with an item, which can be default or not. A marker trait has zero associated items.
| Supertrait kind | auto impl block in sub-trait block | auto impl statement in sub-trait block |
|---|---|---|
| non-marker | Mandatorya | Optionalb |
| marker | Mandatoryc | Mandatoryc |
Case a: auto impl block of a non-marker supertrait in sub-trait block
For illustration, here is an example.
The reason for this is that given that trait Subtrait has already provided its implementation, an implementation of Subtrait must choose between the default implementation and a user-defined implementation. We prefer explicit confirmation through extern impl declaration from the implementor, rather than making the compiler to reason about whether auto impl should be backfilled for ease of language feature implementation.
Extension: Possible relaxation through an attribute and a future-compatibility lint
For important ecosystem traits like PartialOrd and Ord, this rule is still unsatisfactory due to the potential rewrites required on downstream crates, even though it could be as small as an additional extern impl PartialOrd. As an extension, the rule could be relaxed with an attribute #[probe_extern_impl] and apply further trait selection to decide whether the default implementation given by the auto impl block should be used.
// The following code in the ecosystem will continue to compile.
However, this practice will not be encouraged eventually under provision of this RFC. For this reason, we also propose a future-compatibility lint, which will be escalated on a future Edition boundary to denial. The lint shall highlight the existing auto impl block in the subtrait definition and suggest an explicit extern impl statement in the subtrait implementation.
Case b: auto impl block of a non-marker supertrait in sub-trait statement
For illustration, here is an example.
The reason for this is that trait Subtrait has not already provided its implementation, an implementation of Subtrait must supply an implementation of Supertrait, which could have existed before introducing the auto impl Supertrait.
If auto impl statement is declared on a non-marker supertrait without a default implementation, the extern impl is optional so that we do not penalise the existing trait implementors.
Case c: auto impl block of a marker supertrait
For illustration, here is an example.
The reason for this is that Supertrait as a marker trait has no associated items. As we could not decide if the Supertrait would be implemented within the bounds attached to the impl Subtrait block, due to lack of syntatical signals, it is better to require explicit confirmation from the implementor on the condition of the marker trait Supertrait when this marker is applicable to MyStruct.
SemVer consideration
In this section we consider the impact on semantic versioning when a change to trait definition and implementors affects a auto impl syntax structure.
Addition of auto impl in sub-trait definition
This is a SemVer hazard and can constitute a major change to public API, provided that the supertrait relation has not been changed. Implementers now have the obligation to ensure that their external implementation does not conflict with a potential default implementation at the sub-trait definition site.
Removal of auto impl in sub-trait definition
This is a SemVer hazard and can constitute a major change. This requires the downstream implementors of the sub-trait to move the auto impl out of the impl block.
Addition and removal of unsafe qualifier on the auto impl directives
This is a SemVer hazard and mandates a major change. The implementors should inspect their implementation against the trait safety specification and add or remove safety comments accordingly. It is possible that the semantics of the API would change as the safety obligation can propagate through the API across multiple crate boundaries.
Switching between extern impl Supertrait and auto impl Supertrait
This is a SemVer hazard and mandates a minor change. Provided that both the sub-trait and the super-trait remains SemVer stable, this constitutes only a change in implementation detail.
Change in the proper definition of super- and sub-traits
This is a SemVer hazard and mandates a major change. The justification follows API change in trait irregardless of super- or sub-trait relationship. This scenario encompasses any changes in types, function signature, bounds, names.
Drawbacks
- This is yet another new language syntax to teach.
Rationale and alternatives
A auto impl in a trait definition block should not be interpreted as a blanket impl
Our rationale is that this blanket impl would unnecessarily reject genuine user-impls of supertraits where a relaxed where bounds are desirable through overlapping impls. In our opinion, a supertrait impl on a type that has more relaxed bounds than that on a subtrait impl of the same type is perfectly valid.
As illustration, the following is an example
Why explicit opt-in/out for marker traits and traits with only default items?
Marker supertraits and supertraits with default items can be easily overlooked when users write subtrait implementations. They would register too little signal for the reader to recognise the significance of traits of these kinds. For this reason, we bias towards asking users to provide clear syntatical signals through auto impl MarkerTrait or extern impl MarkerTrait, so that the automatic derivation of such traits is easily recognisable and provides obvious site for documentation in case justification is waranted.
// It is almost always a good idea to explain
// why a trait like below is implemented on a type.
What about checking whether another impl exists to decide automatic impl-filling?
We could potentially determine whether the opt-out is used based on whether an impl of the supertrait exists, but we prefer not to. We have existing mechanism to determine the specialisation and implementation overlaps. Whether a supertrait impl overlaps or not, is not the concern of this proposal.
If we would deduce whether an auto impl should be effected, there could present a hazard that silently changes the program behaviour.
Why is a hard error on ambiguity acceptable to us?
We hold the basic assumption that most associated items of a trait have sensible names. We would rather advise that one shall avoid name clashes and ambiguity through better, future-oriented trait designs.
In any case, auto impl Trait { .. } blocks still remains available for cases where ambiguity is unavoidable or favorable in niche scenario.
Why this naming?
It is still up for discussion.
Prior art
Implementable trait-alias
It was suggested in the RFC 3437 that trait aliases can be made to also carry associated items, which in turn can be instantiated in impl trait alias blocks.
Plain supertrait items in subtrait impl
In fact, this proposal is an improved version over this scheme. Previously, to disambiguate names from different supertrait namespaces, one appends the associated item identifies with a qualification and generic arguments when necessary. However, the old proposal would apply more deduction, by the compiler, on whether supertrait impls are demanded and it is a weaker response to the tenet that prefers more syntatical signals to compiler deduction.
Unresolved questions
None so far.
Future possibilities
Think about what the natural extension and evolution of your proposal would be and how it would affect the language and project as a whole in a holistic way. Try to use this section as a tool to more fully consider all possible interactions with the project and language in your proposal. Also consider how this all fits into the roadmap for the project and of the relevant sub-team.
This is also a good place to "dump ideas", if they are out of scope for the RFC you are writing but otherwise related.
If you have tried and cannot think of any future possibilities, you may simply state that you cannot think of anything.
Note that having something written down in the future-possibilities section is not a reason to accept the current or a future RFC; such notes should be in the section on motivation or rationale in this or subsequent RFCs. The section merely provides additional information.