Super trait, why?

I went through this feature chapter from rust book.
I didn't get, why we need this feature. I thought rust will stay away from inheritance.

Where can I find detail about feature ?
Why community thought this is useful feature ?

A "supertrait" is not inheritance.

trait Sub: Super {}

is actually just syntax sugar for

trait Sub
where
    Self: Super
{}

i.e., it's the same old regular trait bound that any other where clause would mean; it doesn't imply anything else. And you should be able to put bounds on any type variable (and any type in general), including Self.


The syntactic sugar is not necessary, of course. One could just write out the Self constraint using the where clause. However, this form of dependent relationship between traits is so common that it was deemed valuable to shorten it somewhat.

2 Likes

Specifically why it isn't a problem like in traditional OO inheritance is down to two things:

  • You can only inherit behavior, not state, so you don't get any of the slicing / diamond inheritance issues like in C++.

  • Impl blocks are completely separate, so Sprite::draw and Gun::draw are totally different methods. At the call site you have to import which trait you want use, and you can always fully qualify, like: Sprite::draw(&object) if you want both in scope.


There is actually a traditional inheritance-like feature, though: Deref in std::ops - Rust allows treating an object of one type as if it was another, and it's recursive, with all the potential issues that inheritance can have. For that reason, it's only intended for use on smart pointers, as the docs say.

4 Likes

Supertraits do give you a few other things:

6 Likes

I’m curious: Given that the traits of a struct are set-ish, wouldn’t Rust avoid the diamond issue even if traits had fields (because a struct couldn’t implement the same trait twice)?

Actually I wouldn't say it's inheritance-like. Or at least not really. While the method lookup as well as deref coercion works transitive (Playground), this doesn't extend to trait implementations (Playground).

So Deref doesn't really establish a supertype/subtype relationship at all.

Moreover, if you attempt to use Deref as a bound, things get even more ugly (Playground).

This is why I wouldn't say Deref is inheritance-like. It allows transitive type coercion and method resolution, but a type B which implements Deref<Target = A> is not a subtype of A and cannot generally be used in places where A is used.

I have been tempted to use Deref for something inheritance like in past, but I ran into problems sooner or later, so I think the advice in the docs to only use it on smart pointers is justified (though the term "smart pointer" is a bit vague).

Generally, to describe common properties of types, traits should be used to describe those common properties. Unfortunately, when you go that way, you can't really "inherit" easily but you often end up to manually forward some method calls. (Though I think there are crates providing macros to aid you here.)

Regarding the original question: "Super trait, why?", this allows a couple of things:

  • Calling the super-trait's method in a default implementation (Playground)
  • Relaxing you from listing the supertraits as explicit bound (Playground)

And maybe there's more. (edit: like @quinedot listed above)


With classic object-oriented inheritance, a Cat is an Animal.

With Deref coercion, it's more like, a String points to a str. (Which, by the way, is why there are a lot of redundant implementations in the std library, e.g. Display for String and Display for str.)

2 Likes

Don't you mean:

-impl<T: ?Sized + Super> Sub for Super { /* ... */ }
+impl<T: ?Sized + Super> Sub for T { /* ... */ }
1 Like

Yep, thanks. Fixed.

Yes. The problems with traits that “have fields” is not any diamond issues. The only problem is language design considerations. The precise nature of such a feature would need to be designed, and it would need to have sufficient advantages about currently available alternatives of using methods on the one hand, and sufficiently few disadvantages compared to possible alternative ways of achieving the same advantages.

Some random/example thoughts of mine on “fields in traits” for illustration that the language design is nontrivial…

E.g. there would be the question whether a field needs to be an actual field, or whether a field-like interface could be somehow “simulated” essentially via accessor functions. If the feature is restricted to exposing actual fields, this would be more restrictive than existing alternatives of providing “getter and setter”-style methods, and it would make the level of abstraction that a trait can offer slightly less. As for advantages, there might be the advantage of reducing boilerplate of accessor functions for implementing the trait, though boilerplate can be avoided with macros, too. The usage syntax also would become slightly shorter, though people might dislike that for being less explicit. Borrow-checking could possibly allow accessing multiple fields mutably in parallel, though that only works if different fields must be distinct; would that apply across traits or only within a trait though? Even then, one could also try to design alternative language features that allow methods to expose more fine-grained borrows.

(There might also be existing discussions on “fields in traits” elsewhere that might cover even more points to discuss.)

4 Likes