Try   HackMD

[Pre-RFC] Supertrait associated items in subtrait impl

RfL context
This enables us to move type Target from trait Deref into trait Receiver without breaking existing impl Deref blocks in Rust ecosystem.

Motivation

This RFC concerns itself with enabling subtrait impl blocks to supply associated items from one or several supertraits.

This RFC proposes an extension so that subtrait impl blocks may designate the associated types from supertraits so that the respective supertraits impl can be implied by collecting the required associated items, without resorting to individual, separate impl-supertrait blocks.

Guide-level description

Supertraits

Supertraits of a subtrait in this context is defined solely in the form of type bound list, as illustrated in Listing 1, as opposed to examples in Listing 2.

Listing 1

Supertrait Super1 and Super2 registered with the subtrait Sub as type bounds

trait Super1 {} trait Super2 {} trait Sub : Super1 + Super2 {}
Listing 2

Trait Super1 and Super2 are not registered as supertraits of trait Sub or Sub2 in these examples

trait Super1 {} trait Super2 {} trait Id<T: ?Sized> { type T: ?Sized; } impl<T: ?Sized> Id<T> for T { type T = T; } trait Sub1 where Self: Super1 {} // Fancy ways to say `Self`s trait Sub2 where <Self as Id<Self>>::T: Super2 {}

impl subtrait blocks today

The way to introduce impl of a subtrait now is rigid, in the sense that one impl block for the subtrait and additional one each for the involved supertrait.

Listing 3
trait Super { type T; fn super_call(&self); } trait Sub: Super { fn method(&self) -> <Self as Super>::T; } enum MyType {} // The disconnect between `impl Sub for MyType` manifests itself // when `type T`, or `<Self as Super>` is literally located in another `impl` block impl Sub for MyType { fn method(&self) -> <Self as Super>::T { self.super_call(); } } impl Super for MyType { type T = (); fn super_call(&self) {} }

There are two possible problems arising from this typical case.

  • The disconnect between contexts, as subtrait impls can refer to items from supertrait impls, but the supertrait impls can be located at other places, even in different modules of the crate. This can impact readability of the code, as readers need to jump extra loops to make the connections.
  • The syntatical red-tape on subtrait impl blocks can become prohibitive as the type signature of Self, the where bounds and the list of generics grow.

A new proposal: merge the super- and subtrait blocks into one

The proposal is to allow users to declare supertrait associated items directly in the subtrait impl blocks.

To build the intuition, Listing 3 could have been written into the following.

Listing 4
impl Sub for MyType { // This is type from `Sub` fn method(&self) -> <Self as Super>::T { self.super_call(); } // This is from `Super` type T = (); // This is also from `Super` fn super_call(&self) {} }

Note that the associated items type T and fn super_call must be resolved to that from the trait Super.

However, this proposal also guarantee that Listing 3 on its own must continue to compile.

One complete set of supertrait items, or none

Rust impl-trait blocks has always been self-containing today. With this proposal, this principle will remain upheld. For an impl-subtrait block to be valid, when it comes to completeness, the following rules must be observed.

  • The subtrait associated items must be all fully defined.
  • If one associated item from a direct or indirect supertrait Super is defined, so are all associated items from the trait Super.

Then in turn, the compiler should guarantee the following.

  • If one associated item from a supertrait Super<..> is defined, it has the same effect as a standalone impl<..> Super<..> block, so that it can be considered that the type bound Self: Super<..> is satisfied.

Commentary

The primary reason that a clash between a subtrait item with a supertrait item is not considered for ambiguity and the item would be resolved to the subtrait item, is due to a property that we would like to uphold, which is to allow the user to easily cut out the supertrait impl block and add into the subtrait impl block without qualifying the subtrait item. The compiler, in this case, should have sufficiently erred about the name clash due to pasting in the supertrait items.

Positive examples

Under this proposal, these are the notable valid examples.

Listing 5
// == Prelude == trait Super1<T> { type Super1Type; } trait Super2 { type Super2Type; } trait Sub<T>: Super1<T> + Super2 { type Type; } // == Example 1 == enum MyType1 {} impl<T> Sub<T> for MyType1 { type Type = u8; // .. implying an impl Super1<T> type Super1Type = u32; // .. implying an impl Super2 type Super2Type = (); } // == Example 2 == enum MyType2 {} impl<T> Sub<T> for MyType2 { type Type = u8; // .. implying an impl Super1<T> type Super1Type = u32; // Note that we have not declared anything // from Super2 yet } impl<T> Super2 for MyType2 { type Super2Type = (); }

Negative examples

Under this proposal, the following should not compile.

Listing 6
// == Prelude == trait Super1<T> { type Super1Type; // Note that we have a second associated type type Super1Type2; } trait Super2 .. trait Sub<T>: Super1<T> + Super2 .. // == Example == enum MyType1 {} impl<T> Sub<T> for MyType1 { type Type = u8; // .. implying an impl Super1<T> type Super1Type = u32; // ^~~~~~~~~~ // error[E0046]: not all trait items from `Super1` implemented, missing: `Super1Type2` // .. implying an impl Super2 type Super2Type = (); }

Disambiguation

In case that there are clashes on associated item names among supertrait and subtrait items, attempting to declare one of them from supertraits without qualification must be rejected.

Positive example, when the item can be resolved to a subtrait item

Listing 7
trait Super1 { type Type; } trait Super2 { type Type; } trait Sub: Super1 + Super2 { type Type; } enum MyType1 {} impl Sub for MyType1 { type Type = u8; // OK }

Negative example

Listing 8
trait Super1 { type SuperType; } trait Super2 { type SuperType; } trait Sub: Super1 + Super2 { type Type; } enum MyType1 {} impl Sub for MyType1 { type Type = u8; // OK type SuperType = u8; // error[???]: ambiguous associated item // could be `Super1::SuperType` or `Super2::SuperType` }

Positive example

To resolve the ambiguity, one must introduce path qualification of the targeted trait in front of the associated item identifier. If the targeted subtrait or supertrait has generics, the generics should be included in the path.

Listing 9
trait Super1 { type Type; fn method(); fn super_method1(); } trait Super2<T> { type Type; fn method(); } trait Sub<T>: Super1 + Super2<T> { type Type; fn method(); } enum MyType1 {} impl<T> Sub<T> for MyType1 { type Sub::<T>::Type = u8; // OK type Super::Type = u8; // OK type Super::<T>::Type = u8; // OK fn Sub::method() {} // OK fn Super1::method() {} // OK fn Super2::method() {} // OK fn super_method1() {} // OK, and this is for impl Super1 for MyType1 }

Inductive Supertrait relation

Let us suppose Sub trait has a supertrait Super1, which in turn has a supertrait Super2.

Listing 10
trait Super1: Super2 { type Type; } trait Super2 { type Type; } trait Sub: Super1 { type Type; }

Then, this proposal enables the impl on subtrait with various depths of declaration.

Listing 11
enum MyType1 {} impl Sub for MyType1 { type Sub::Type = u8; // OK type Super1::Type = u32; // OK type Super2::Type = (); // OK } enum MyType2 {} impl Sub for MyType2 { type Type = u8; // OK } impl Super1 for MyType2 { type Type = u32; // OK type Super2::Type = (); // OK }

Reference-level description

The following is a suggestion on possible compiler implementation.

AST extension

We will now allow path elements and generics in the associated item location. As far as it would look like, introduction of grammar ambiguity is unlikely.

Name resolution

Given that identifiers can be foreign to the impl, and the associated item resolution is eager, we would like to one extra phase. The exact work requires a quick survey and prototyping.

The work here is to make sure that the respective DefIds of supertraits implied impls shall be procured. In this way, no changes to trait solving are required, which is very critical to the success of implementation of this language extension.

We will defer the span information, which is critical to diagnostics, to a later phase of the prototyping.

Drawback

Why should we not do this?

Introducing possible magic

Now unless the identifier names are very self-evident, the supertrait associated items may be seen too magical as they would appear very irrelevant to the impl block.

One could get confused if one is not very familiar with the trait definition and when subsequently encouter a supertrait item that has not logical connection to the rest of the subtrait impl block.

Enablement of mis-organisation

One can foresee that given this option, users may elect to flood a subtrait impl block with all supertrait declaration. It has been a long appreciated tradition of Rust that code should be organised and partitioned by pragmatics and functions. There is a risk that the abuse of this language extension will break this nice property.

Prior arts

The author has not been made aware of prior discussion.

Unresolved questions

  • If we want to proceed, how do we develop tools to help users contain unnecessary encroachment of supertrait items into subtrait impl blocks?