Feature proposal: implementing multiple traits at once

This is an idea I had that I don't have the bandwidth right now to write a RFC for, so I'm just throwing it out there to see if it gets interest.

Summary

You should be able to implement multiple traits at once in a single impl block, as long as they form a supertrait chain.

Example syntax:

use std::ops::{Deref, DerefMut};

struct DerefMutExample<T> {
    value: T
}

impl<T> Deref + DerefMut for DerefMutExample<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.value
    }

    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.value
    }
}

Rationale

This would just be syntactic sugar for multiple implementation blocks.

This is especially convenient for some pair of traits (eg Deref + DerefMut, PartialEq + Eq, PartialOrd + Ord) where the second trait is basically a refinement of the first, with almost the same semantic meaning. In those cases, it makes sense to implement all the traits in the group at once, because both implementations "express" essentially the same concept.

In particular, some "refinement" traits (eg ExactSizeIterator) can be implemented without any methods. Just writing impl Iterator + ExactSizeIterator feels more expressive that having make it a second block.

Deeper implications, potential breakage

No idea.

3 Likes

Subtraits can define items with the same name as in the supertrait, so that case needs to be handled in some fashion (probably just by disallowing bundling those implementations).

3 Likes

And, ideally, in some fashion that doesn't make adding methods to the supertrait a breaking change.

(Especially after RFC: Supertrait item shadowing by lcdr · Pull Request #2845 · rust-lang/rfcs · GitHub was accepted to make this a bit less of a problem.)

4 Likes

Subtraits items are constrained to be "subtypes" (I don't remember the exact term) of the eponymous item in the super-trait, right?

Let's see...

trait Super {
    fn foo(&self, i: i32);
}

trait Sub: Super {
    fn foo(&self, x: &f64);
}

Nope. This compiles perfectly fine.

Ugh. The feature sounds a lot less elegant now.

I guess the simplest way to implement bundled implementations would be to only allow them if the trait definitions match both trait's items. That would make adding methods to the supertrait a potential breaking change though.

Does this provide any value other than syntactic sugar? I feel like the expressive value of implementing related traits simultaneously gets overshadowed by the anti-didactic value of the new construct.

4 Likes

This makes trait impls significantly less greppable. At the moment if I want to search for trait impls, and the trait isn't generic, I can just search TraitName for , and find all usages. With this proposal grepping for impls becomes impossible, because even if a trait has no supertraits, someone downstream can always add a subtrait, and then write impl TraitName + SubTrait for Type.

Note that greppability is still important in the age of IDEs: an IDE may still be not available in many contexts (like online code review or docs), IDEs have bugs, and it's impossible to search for impls within macros using an IDE (at best you'll get the call site of the macro, but the macro definition itself may span thousands of lines, and with proc macros it's even worse).

Given that this syntax sugar basically saves just a line of impl header (impl<Ts..> Trait<Ts..> for Type<Ts..> where conds), I say that the cost-benefit slider is heavily on the "cost" side.

9 Likes

Why do you call it anti-didactic?

When learning Rust, you must spend extra time just to learn how to use this feature.

2 Likes

Yeah, that's a pretty strong argument. That downside beats any upside from the added expressiveness.