Idea: Layout inheritance once more: an easier way

Hello, this is a real old topic, and i want to talk about it again. I’m sure you’ve read RFC349 and RFC1546 on this topic, which both exhibits some real powerful way as a feature - too powerful that they’re repeatly postponing. So I’d like to try something different: a minimalist approach.

First i’ll assume that we want to build this feature as a library, maybe implemented using proc macros. This gives some flexibility for community experiment. And all we want to talk about is what support we want from the language side.

Then let’s see what we want to get. Something in par with single inheritance in C++ is quite enough. Multiple inheritance is undesirable since it will bring up the diamond inheritance problem. So what’s essential behind single inheritance. It’s just a reusable sub-object of the base class in the struct instance supporting cheap casting between the two. Putting the delegation issue aside, and that’s all.

So what we can expect from the language is: an field attribute that tells Layout to put this field just at the beginning of the memory segment of the value. (offset_of == 0) It can only occur once in a struct. Something like this:

struct Foo {
    #[zero_offset]
    pub base: Bar,
}

And that’s (almost) all ! The compiler should ensure that it is defined behavior to cast &Foo to &Bar. It should also ensure vice versa if the Bar is actually living as a base field. So such transmutation methods can be provided by a library.

I said almost because actually there’s another issue in it: ZST issue. If Bar is ZST and Foo is not, you can’t always cast Bar back. This can be resolved by special rules and hacks, i believe. But i know too little to talk about them.

With such support. I believe people can just write a proc macro to do whatever structural reuse they want.

What do you think?

1 Like

How do you propose to ensure that “Struct Slicing” does not occur?

1 Like

At this point you don’t even need inheritance. Just embed a type anywhere in your struct, and implement Deref:

struct Foo {
    base: Bar,
}

impl Deref for Foo {
    type Target = Bar;

    fn deref(&self) -> &Bar {
        &self.base
    }
}

If you do this a lot, you might want to write a macro for it. A simple declarative macro-by-example can expand to the impl Deref part, not even a proc-macro is needed.

Although one might argue that this is abuse because it effective emulates inheritance whereas Rust doesn’t have inheritance and that is intentional; however, by those measures any form of inheritance could be considered abuse. I wouldn’t recommend doing this, because inheritance is painful (to implement and use correctly), and it’s surprisingly easy to get around the “need” of using inheritance in Rust due to its very expressive type system. But there you have it, in case you want something like that, you can have it today, by using existing language features.

2 Likes

I'd say it is a little abuse since the Deref slot is "occupied" and you can't use it for anything else now. But maybe that's not a big problem.

The main problem is that this is only upcasting and (somewhat) delegation support. An another equivalent important feature is downcasting. It is not very ergonomic to put the type cast stuff into the traits (or use Any, which is very restricted too) than just give the ability to downcast according to some convention. And the problem is that, you can't assume the field's offset_of value is 0. Thus the zero_offset attribute.

The rest is not about #[zero_offset] But for reference, I'd like to show a different design scheme than yours, which looks like this:

struct GenericAnimal<SubClassMixin: Sized> {
     #[zero_offset]
     pub base_data: AnimalBase,
     mixin: SubClassMixin,
}

struct DogMixin;
struct CatMixin;

type Dog = GenericAnimal<DogMixin>;
type Cat = GenericAnimal<CatMixin>;

impl<Mixin> GenericAnimal<Mixin> {
     <common methods here>
}

impl Dog {
     <dog methods here>
}

impl Cat {
     <cat methods here>
}

I think this can work fine most of the time, the only missing part is this #[zero_offset] which enables the use of AnimalBase acting as a type-erased Animal. Though i've not written serious code using such pattern yet(only in some testing code did i use this pattern). The delegation part and specialization part are bonus points, but i don't miss them a lot.

And for fun, there're even more design possibilities with the "mixin" as an enum or a generic struct .

@gbutler I think we always see AnimalBase in borrowed state... Even if you moved it or cloned it you won't get a GenericAnimal<T>, right?

1 Like

As I mentioned, it could be considered abuse, but honestly, I would consider any sort of inheritance an abuse in Rust.

Downcasting is itself an anti-pattern. If you need to downcast an inherited type, then what you are really looking for is an enum. If that’s the primary reason Deref is not sufficient, then I think this sort of inheritance promotes bad practice and is thus actively harmful to the language.

3 Likes

I don't totally agree with that. Enums works for 'closed' categories of taxonomies, and generics works for 'open' categories of them. While Rust can't express a borrowed generic-typed value with erased parameter type without touching the trait system, yet, i think it's perfectly valid for people to try do express such designs with existings tools, instead of waiting till, say, 2020. I don't think there's a sin behind such technical decision. For example, how could you know what beast the downstream crates are going to add to the taxonomy upfront?

Off-topic:

While i value and appreciate the clean, high performance design "casual" Rust designs are, I also value the ability to re-express the existing designs. While it's fine to exile them and make them non-ergonomic and tell people to avoid them as much as possible, it's important not to extinguish the possibility at all. Work must be done any way.

2 Likes

Honestly I don’t think it’s that bad to use Deref to simulate inheritence. It looks hacky, yes, because inheritence is hacky.

With this in mind, Rust already has built-in concept of inheritence. Deref and DerefMut is upcasting, and Any is downcasting. Like other OO-first languages, upcasting always success but downcasting can fail, but we get None instead of throwing TypeError. There’s only single inheritence as you can implement Deref for your type up to once.

At this point one can ask how can we implement upcasting to middle type, which is more specific than Any or Object, but more abstract than the end struct. Its simple, because Any is just trait, you can make Box<Any + YourTrait>. It can be painful to do it all by hands, but I saw some crate there just for this work(but i forgot its name :P).

1 Like

But if you want to allow an arbitrary, open set of values, how do you expect to downcast at all? You can only downcast to one or more specific types that you bake into your code. The point of downcasting is exactly that you know the specific type you are trying to convert a more generic type to.

Why is "touching the trait system" considered bad? Generics with trait bounds are exactly the mechanism which are designed to handle "arbitrary" (open) sets of types in Rust. Why wouldn't you use a tool that is designed specifically for the job?

Yes, and even if that is not the case, I don't see value in trying to write "traditional OO", Java-like or C++ like code in Rust. Especially if you have to effectively rewrite the code anyway, why would you force a style on the language that was deliberately omitted from it?

I write this down under another inheritance proposal, but here we go again: Rust had classes and inheritance before 1.0. It was removed because the language team and the community decided that inheritance did more harm than good. Again, it might sometimes be useful, but in more cases it complicates matters significantly, and it is partially redundant with traits.

I somewhat disagree with this. I would prefer a more opinionated language. Make things that are anti-patterns impossible or nearly impossible and have the guide educate about the "correct" thing to do instead. Opinions will definitely differ on this though.

1 Like

Hi, thanks for the reply!

Through common features, of course. I've come up with two approaches:

  1. They may share a common set of operations. (Abstract type / Trait approach).

    By this i mean the full type-erased type, namely the dyn Trait type. Actually it is very handy for tempoarily-borrow-and-use scenerios, i appreciate it.

    But it quickly become very stubborn when it become your responsibility to own and store it. No when it's unboxed it doesn't work at all since it's unsized type. Even with unsized rvalue implemented, you still can't store it into a field, a Vec, etc. It won't work. When it's boxed, you'll reach the limit that it can actually implement one trait at a time. If you can't put everything you need in a single trait, you'll need to generate other corresponding trait objects. And then they'll have to be either boxed or borrowed, which gives different trade-offs. Very stubborn.

    Then the impl Trait type, it can't live without deduction, and is almost no functionality addition to the language expect when the type is anonymous. So we don't talk about it here.

  2. They may share a common storage "header" or "shell"(generic struct). (Concrete type approach)

    See my design in the previous above as an example. Let's talk about this below.

The trait system itself is actually quite good. but when you don't know enough about the concrete type, traits can only act as proxies. It works perfectly well when you write a trait for yourself or your team to use, but not always so. When you receive things and are operated on, just define a trait. (think Future). When you provide implementations, use concrete types. (think Executor). There's a whole lot of the later case, and defining traits there are optional but not a necessary.

"(It) did more than good" as a language feature. I'd say nobody yet wants to force such a style. What i suggested in the top post is a zero-cost feature, that asked Rust's structure layout algorithm to explicitly position a field first so that we can know that such a field will have a fixed offset_of value, so it's possible to reliably find the structure containing such a field, making reference transmutation defined behavior. I can't see why such a small feature can force the language into some style...

For the library part, it's completely disassociated with Rust the language, only some experiment as a crate, right? I don't see how this is different from gnome-class or other crates. Why would you feel a crate can be actively harmful?

Sorry, might be off-topic but i feel that Rust is already enough opinionated in its current state of art.

We've already got things like feature gate (Which means you can't do anything associated with some name before it is unfrozen in the distant future), We've already got things like orphan rules.(Which means for example you actually can't use json without serde, and it's YOUR responsibility to define serde interoperability for your crate, because if serde doesn't support your crate actively, according to orphan rules others won't have any chance to do so.) We've got so many people eagerly waiting for the one true futures-1.0. (EDIT: No this is off-topic)

No i'm not even complaining, i accept these as facts and i love Rust. But for productivity, let's make things at least migratable. Honestly I think very few people in the world know how to design large piece of software in Rust. If Rust doesn't support other system's existing software architecture and say "No you have to redesign it in the true Rust way". Then the process will be really risky, much much riskier than asking a colleague to do some file-by-file port experiment. Right?

I’ve completely lost track of what concrete suggestion this thread was supposed to be about. There was some talk of macros and libraries, but I have no idea how a #[zero_offset] macro or anything like it would be possible to implement in a library. If this is about some sort of language feature to manually control layout, then we have to talk about all the virtual struct/field in trait ideas over at https://github.com/nikomatsakis/fields-in-traits-rfc, and tbh I have no idea what the status of any of that is (it feels like it’s simply been abandoned since everything on the 2018 roadmap is a higher priority). If this is about some “layout 1.1” minifeature to tide us over until a full solution, I’m not sure that’s feasible because stabilizing a subset of anything requires a strong consensus on the general structure of the complete solution, which I’m pretty sure we don’t have for virtual structs. If this is about autogenerated enum types or non-exhaustive enums or enum impl trait or any of the other variations on extending enums, there’s existing discussions on all of those too. If this is about something completely different, then we really need to clarify what that something is.

2 Likes

Please explain how this really differs from OO languages? I don't see it. If I create an interface or base-class in an OO language, the only thing that can implement it for some particular class is that class. I can't implement an interface or base-class for some arbitrary class (whether I'm the definer of the base-class or interface). So, the situation with traits is actually more flexibility as to who can implement a trait for a type than you have for who can implement a base-class or interface for a class.

1 Like

At least we have #[repr(C)]. So it can be implemented as a custom derive.

@lxrec Thanks.

I was just seeking comments about whether

  1. the #[zero_offset] mini-feature is neutral, lightweight and harmless enough to be included in the language.
  • You made it clear to me that i need a more individual motivation and this design as a individual complete solution, now i understand it better. Thanks a lot.
  1. people think my approach of modelling a taxonomy on a later post above is feasible and satisfactory.
  • Some other people above think #[zero_offset] doesn't have legal use cases, as any such intention to model Java-style inheritance in Rust should be strictly forbidden. I do have such intention, and i still don't really agree with them.

Thanks for the tip. I don't understand #[repr(C)] well, I think maybe it'll work, just a little...awkward.

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