Supertraits vs Generic Implementations

This is a topic about Trait “deduction”: for all Type: TraitA, we want Rust to know that we also have Type: TraitB

Currently, there are 2 ways of achieving this:

  • Generic implementations:

    //! crate `generic_impl`
    pub trait TraitA { /* ... */ }
    pub trait TraitB { /* ... */ }
    
    // Generic implementation providing `T: TraitA => T: TraitB`
    impl<T: TraitA> TraitB for T {
        // implementation of `TraitB`'s associated functions / types,
        // only with access to / knowledge of `TraitA`'s
    }
    
    extern crate generic_impl; use ::generic_impl::*;
    
    struct SomeType { /* ... */ }
    
    impl TraitA for SomeType {
        // implementation of `TraitA`'s associated functions / types,
        // without access to / knowledge of `TraitB`'s
        // but with access to `SomeType`
    }
    
    fn assert_implements_TraitB<T: TraitB> () {}
    
    static assert_SomeType_implements_TraitB: fn() =
        assert_implements_TraitB::<SomeType>;
    

    Examples: (use ::std::*;)

  • Supertraits: https://doc.rust-lang.org/book/2018-edition/ch19-03-advanced-traits.html#using-supertraits-to-require-one-traits-functionality-within-another-trait

    //! crate `supertrait`
    pub trait TraitB {}
    
    // Supertrait (access to `TraitB`'s functions/types while impl-ing `TraitA`)
    pub trait TraitA : TraitB { /* ... */ }
    
    extern crate supertrait; use ::supertrait::**;
    
    struct SomeType { /* ... */ }
    
    impl TraitA for SomeType {
        // implementation of `TraitA`'s associated functions / types,
        // with access to / knowledge of `TraitB`'s
        // and with access to `SomeType`
    }
    
    // Error: `TraitB` is not implemented for `SomeType`
    

    Examples: Copy: Clone, Fn: FnMut, FnMut: FnOnce

Now, even though both give the same “deduction rules”, they do it differently:

  • in the former case, you get Type: TraitB for free and, unless the generic implementation is just a default (overridable) one (cf. specialisation RFC), it cannot be overriden;
  • in the latter case, you have to provide the Type: TraitB implementation yourself;

Now, take the case of Copy : Clone (or Fn : FnMut : FnOnce), what is the reason behind having Copy be a supertrait of Clone (i.e. Copy: Clone) instead of having a generic (default) implementation of Clone for all T: Copy ?

  1. When implementing the Copy marker trait for a type, it’s weird that we have to explicitely give the obvious implementation |&self| *self;
  2. It is even weirder (in this particular case), that we are allowed to give a different implementation!

Obviously, (2) is subjective, and with specialisation and default impls, we can still allow it (and thus avoid breakage of Clone impls) while getting rid of (1) if we replaced the extension Trait relation with a generic impl.
And the exact same thing could be said of Fn : FnMut : FnOnce shenanigans.

Finally: for those saying that it’s no big deal since we have #[derive(Clone, Copy)], you should know that:

  1. derive's heuristics are not very smart (One example)
  2. There is (currently) no #[derive(FnMut, FnOnce)] for a struct for which we implement Fn
3 Likes

Consider a generic type that should implement both Copy and Clone when possible:

#[derive(Copy, Clone)]
struct Foo<T>(T);

The derived implementations expand to something like this:

impl<T: Copy> Copy for Foo<T> {}
impl<T: Clone> Clone for Foo<T> {
    fn clone(&self) -> Self { Foo(self.0.clone()) }
}

The Clone impl here would conflict with a blanket impl<T: Copy> Clone for T.

Specialization could resolve this conflict, but it wasn’t available back when the supertrait bound was added:

2 Likes

Good point; specialization is indeed required / needed.

I then hope that once specialization becomes stable, the supertrait will be replaced by a generic (default) impl

Or option 3: both. This is used in futures for FutureExt and other extension traits.

pub trait FutureExt: Future {
  /* ... */
}

impl<T: ?Sized> FutureExt for T where T: Future {}

I’m not sure whether this provides an advantage over just a generic implementation. Maybe somehow related to trait objects?

What you are doing is different (extension traits, as you pointed it out yourself): adding functionality to an existing trait by creating an equivalent trait: FutureExt: Future implies that if T: FutureExt then T: Future and your impl ... implies that if T: Future then T: FutureExt.

A very neat trick for sure but not the one I am asking for, with a strict “sub-Trait” relation (not all Clone objects are Copy)