Blog post: specialize to reuse (a pre-RFC for "efficient inheritance")

TL;DR: specialization supports clean, inheritance-like patterns out of the box. This post explains how, and discusses the interaction with the "virtual structs" saga.

http://aturon.github.io/blog/2015/09/18/reuse/

Comments very welcome!

4 Likes

Just a very simple question. Does this imply that we get rid of default implementations in traits? As far as I understand any default implementation in a trait could be turned into a partial implementation for a generic parameter. So this may introduce redundancy between features if we keep both.

Under this scheme, default impls would live on as sugar for partial impls.

Do you plan on making impl Node {} work with DSTs? Currently, given impl Node for SomeDST {}, I can’t cast &SomeDST as &Node so I can’t call anything implemented in impl Node {}.

Hm, I may be misunderstanding, but this seems to work fine today (see this play example). In particular, we have that impl SomeTrait for SomeTrait automatically for every trait, which is all you should need to make the impl you're mentioning work. That doesn't involve any casting; the call to methods on the second trait is actually statically dispatched.

In your post, you had impl SomeTrait {} (no for ...) for non-virtual methods but those methods won’t be callable on DSTs:

trait Foo {}

impl Foo {
    pub fn foo(&self) {}
}

impl Foo for [u8] {}

fn main() {
    let f = &[1u8,2,3] as &[u8];
    f.foo(); // Lookup error
    (f as &Foo).foo(); // f does not implement Sized.
}

I wanted to note one twist on @aturon’s original idea. One of the things I find somewhat…surprising about a #[repr(thin)] trait declaration is that “thin-ness” is a property of both the trait and the implementing type. That is, if you implement a #[repr(thin)] trait, it affects the layout of your struct, since we must insert a vtable. It feels a bit surprising somehow that this results just from implementing a #[repr(thin)] trait.

Orthogonally, there has long been a desire for “inherent traits” – that is, traits whose methods act like inherent methods for the purpose of method lookup, so that if I want to call a trait method bar on an instance foo of a type Foo, I would do not need the trait imported, I can just do foo.bar(). This would allow for, among other things, migrating from inherent methods to trait methods without duplication. The problem is that these inherent traits need to be declared alongside Foo.

In some earlier thoughts (which were similar to what @aturon described), I was considering killing both those two birds by saying that structs could declare a set of bounds that are exactly the same as the bounds on a type parameter or trait, meaning that they can include both other structs (as @aturon described) as well as traits. These traits would be called the inherent traits of a struct:

struct SomeStruct: SomeTrait + Clone { 
}

#[repr(thin)]
trait SomeTrait { }

As a result of a declaration like the one above, three things would happen:

  1. The methods from SomeTrait and Clone would be available as inherent methods for instances of type SomeStruct.
  2. It would be an error if SomeStruct did not implement both SomeTrait and Clone elsewhere.
  3. Because there are thin trait(s) included in the inherent bounds of SomeStruct, its layout is modified to include an extra vtable.

The rules on #[repr(thin)] traits would be as follows:

  1. #[repr(thin)] traits can only be impl’d by types that declare the trait as an inherent trait
  2. In any list of bounds, all #[repr(thin)] traits must form a hierarchy, so that we can layout the vtables in a simple way.

Anyway, I’m still trying to formulate my full thoughts here, but I wanted to toss this idea out there.

The issue here is that you cannot base an object on an unsized type ([u8], in this instance). I think the intention of @aturon’s design was that you only implemented the various traits on the leaf structs, which are indeed sized.

@aturon:

One idea that has propably been mentioned before: Both this proposal and a lot of others seem to have something a long the lines of Node with NodeMethods or NodeData with Node hence would it be worth having both trait fields and struct composition/inheritance together? Or some way of specifying a “default” struct?

In addition, you could say that Rust already has enough features — or enough proposed features — to add cheap field access from internal methods. One thing could be to add an unsafe trait that states that an instance of a type is located at a certain offset of another type, then to implement deref for Box<Trait+UnsafeFastDeref>. Some sort of fast deref trait could work well with the struct composition plan as well. Another idea would be to add some sort of #[parent] attribute which facilitates this — or makes the struct composition idea more explicit.

Thanks for the feedback and ideas!

If I understand what you mean, I think the section about trait fields in the post gives one such way to do it. (Though part of the point is to eliminate the need for struct inheritance.)

I'm leery of any solution to these problems that requires significant use of unsafe, though; it feels like a bit of a cop-out. We should be able to capture these patterns in safe Rust, IMO.

Yes, that seems reasonable, although to be honest I am pretty unenthusiastic about the forced-composition approach I proposed in the post; it just seems to put principle over pragmatics in a way that doesn't buy you much in practice. If one struct is effectively inheriting from another (in terms of data and identity), it seems reasonable to say so explicitly rather than encoding via composition that's not really used as such.

Thanks for replying!

I was thinking that #[parent] or the struct inheritance synax could be sugar for this. Doing something like this might (although I'm not really sure) enable people to easily switch between cheap field access and dynamic field access. Personally I think being able to switch between these two things would add a small bit of flexibility that could be useful.

If it were deemed useful in some type of inheritance hierarchy, do you think that dynamic field access would be at odds with struct inheritance? I suppose though that something like this could be just extra complication for no benefit.

The trait field part of the proposal is very appealing to me. It doesn’t require changing what is currently inheritable in rust, which is traits. And it simultaneously matches up with it’s particular use case of accessing fields from traits and trait objects. If attempting to add direct field access with fixed offsets and inheritence hierarchies I think the simplest mental model and change to current rust code would be to just allowing using field prefixes.

The downsides as mentioned would be questions about trait field visibility and other unknown implementation details.

I’ve only been writing rust for 4 months, although very heavily, and I just wanted to at least weigh in on this particular issue. Because most of my hierarchies are very shallow, I have often longed for simple trait fields and field access. The extra boilerplate of abstract classes is very unappealing to me. When reading this proposal, when the option was mentioned I immediately smiled.

Thanks for listening.

I'd really like that rust does not include struct inheritance... however I must admit that I found none of the designs proposed in the "the trait-based approach" for handling fields really appealing. Maybe my understanding of how inheritance and Rust could interact is yet too limited but still I have the feeling those propositions are degrading the rust model. An aspect I really like for now is that Rust offers a better conceptual separation of roles than in many other languages:

  • to define data you use types (structs/enums)
  • to define interfaces you use traits (and as a consequence traits are the place where dynamic binding become possible)
  • to define actual behaviour you use implementations

For example I'm happy with the fact specialization makes trait default syntactic sugar for default for specialization because actual code for methods conceptually belong to implementations so that's where they should be (and in general I'm happy with most of the ideas proposed here).

Precisely I don't feel that the designs for handling fields achieve orthogonality (I'm not considering enum-based approach). Declaring fields in a trait is obviously injecting data in interfaces. Inheriting structs is using data in place of interfaces (because structs become bounds) but also implicitely attaching code to data (as I suppose method are implicitely imported from super-struct to sub-structs, am I correct ?). The struct composition has the advantage of defining "inherited" fields inside the sub-struct. That means types handle data again. But then struct are reused as bounds for traits.

I know many here are concerned with cheap access to fields in case of dynamic bindings and then statically-known location is presented as highly desirable with DOM being the unavoidable usecase. However I sometimes have the impression that it forces Rust to adapt to an external programming model (and to be clear I'm skeptical on the "language independent" argument promoted by DOM; but that's another story).

Some relevant thoughts here: http://smallcultfollowing.com/babysteps/blog/2015/10/08/virtual-structs-part-4-extended-enums-and-thin-traits/

I like the struct inheritance. Perhaps we can also have enum inheritance like

enum SomeEnum {
      A,
      B,
}

// implicitly contains all of `SomeEnum` variants
enum ExtendedEnum: SomeEnum {
      C,
      D,
}

// this method can accept `SomeEnum` or `ExtendedEnum` as the parameter
fn some_method<T: SomeEnum>(t: T) -> ExtendedEnum { t }

// same as writing `trait SomeTrait where Self: SomeEnum`
trait SomeTrait: SomeEnum { /* ... */ }

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