Idea: Inline supertrait implementations

In Rust, many traits come with 'subtraits' that refine and/or further constrain them. For example, in core: Deref/DerefMut, PartialEq/PartialOrd, Eq/Ord, etc... This allows maximum flexibility, but comes with several downsides:

  • For implementors, this leads to boilerplate: one need to provide a separate impl block for each supertrait, which means repeated elements like generics parameters and where clauses. This can get pretty painful for traits at the top of complicated hierarchies, like Ord.
  • For library authors, 'splitting off' a super-trait from a pre-existing trait is a breaking change, which can hinder API evolution.

Here's my solution for both of these issues: what if a trait and its supertrait(s) could be implemented together in a single impl block?

// This will implement both `Deref` and `DerefMut`.
impl DerefMut for Foo {
    // Declaring the supertrait's (`Deref`) items inline: all items must be present.
    // The supertrait must still be implemented separately if its items aren't provided here.
    type Target = Bar;
    fn deref(&self) -> &Bar { ... }

    fn deref_mut(&mut self) -> &mut Bar { ... }
}
2 Likes

If this is to be made possible, I expect that it'll be necessary to mention both traits in the impl header, i.e.

impl Deref + DerefMut for Foo {

or that at a minimum this would require an opt in on the subtrait that it can be implicitly combined with the supertrait impl. Otherwise you break the current pattern of sealed traits via an empty supertrait impl which cannot be written outside of the source crate due to path visibility.

Pulling off trait items to a supertrait is breaking even if the impl block doesn't need to change, unfortunately, as you can name Trait::item directly, which would no longer be valid if the item is moved to a supertrait.

The fully general form combines some amount of "impl sets," partial default impl, and delegation/reexporting. But a specific targeted feature to enable extraction of supertraits would be useful separately from those, though it would still need to solve a significant subset of the problems involved in those.

1 Like

A super-trait can add a new method with a default body, which today (sortof) isn't a breaking change, after 2845-supertrait-item-shadowing - The Rust RFC Book

But that would start to be breaking again if one could implement multiple traits in the same block.

(Which is a shame, because I really do wish that splitting up traits could be made non-breaking.)

I don't think this is a real blocker; the same shadowing rules could also apply in the impl block (i.e.: in case of ambiguities, always pick the most specific trait). This implies that instances of shadowing between a trait and its supertrait(s) will require going back to separate impl blocks, just like cases today where adding new inherent methods conflicting with downstream traits may require extra disambiguation at call-sites. In particular, one possible resolution could be:

  • If an item in Trait shadows a non-default item in Super, require separate impl blocks;
  • If an item in Trait shadows a default item in Super, interpret the name as only implementing the item of Trait, and emit a warn-by-default lint;
  • If there are multiple supertraits declaring items of the same name, require separate impl blocks.

IMHO this robs the feature of much of its interest (the possibility of splitting of a supertrait while staying semver-compatible); if some extra syntax is necessary, the opt-in on the subtrait would be my preference.

W.r.t. the sealed trait pattern, note that as long as the sealing trait doesn't have any items (including default ones) privacy isn't broken, as you need to mention at least one item of the supertrait to trigger the 'inline impl'.
The proper way of dealing with pub-in-private traits would be to teach rustc about them, so that the 'inline impl' feature can be disabled for these supertraits; I don't know what the implications of such a change would be.

Pulling off trait items to a supertrait is breaking even if the impl block doesn't need to change, unfortunately, as you can name Trait::item directly, which would no longer be valid if the item is moved to a supertrait.

Would there be any significant downsides in allowing Trait::item to work with items in supertraits? After all, you can already write things like String::deref, so allowing DerefMut::deref and <String as DerefMut>::deref doesn't seem such a big jump.

What if we required an annotation on the subtrait to allow merging impls with the supertrait?

Possibilities include:

  1. An attribute: #[allow_merge(A)] trait B: A {}
  2. A keyword: trait B: default A {}

I feel the attribute is more flexible. By doing this, we can extend the attribute’s power to cover explicit merged trait implementations as suggested by @CAD97, but even outside of supertraits:

#[allow_merge(B, C)]
trait A {}

#[allow_merge(A, C)]
trait B {}

#[allow_merge(A, B)]
trait C {}

impl A + B + C for () {}

A #[merge_group(ident)] attribute might be more ergonomic, but it could be that we’d only want to encourage this for tight-knit deeply interrelated small groups of traits (anti-ergonomics à la raw pointers)

Note: this post was edited because I didn’t actually read the full thread so I suggested an idea that was already proposed.

In case of ambiguity one could qualify the fns, like fn Deref::deref(&self) -> &Bar and fn DerefMut::deref_mut(&mut self) -> &mut Bar (if they had the same name)

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.