rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.81k stars 1.55k forks source link

Efficient code reuse #349

Open nrc opened 9 years ago

nrc commented 9 years ago

Motivation

Data structures which closely fit a single inheritance model can be very efficiently implemented in C++. Where high performance (both space and time) is crucial there is distinct disadvantage in using Rust for programs which widely use such data structures. A pressing example is the DOM in Servo. For a small example in C++, see https://gist.github.com/jdm/9900569. We require some solution which satisfies the following requirements:

There has been discussion of potential solutions on discuss (http://discuss.rust-lang.org/t/summary-of-efficient-inheritance-rfcs/494) and in several meetings (minutes and minutes).

We clarified the requirements listed above (see the minutes for details) and established that an ergonomic solution is required. That is, we explicitly don't want to discourage programmers from using this feature by having an unfriendly syntax. We also summarised and evaluated the various proposals (again, see the minutes for details). We feel that no proposal 'as is' is totally satisfactory and that there is a bunch of work to do to get a good solution. We established a timeline (see below) for design and implementation. We would like to reserve a few keywords to reduce the backwards compatibility hazard (#342).

Plan

In December the Rust and Servo teams will all be in one place and we intend to make decisions on how to provide an efficient code reuse solution and plan the implementation in detail. We'll take into account the discussions on the various RFC and discuss comment threads and of course all the community members who attend the Rust weekly meetings will be invited. We will take and publish minutes. This will lead to a new RFC. We expect implementation work to start post-1.0. If we identify backwards compatibility hazards, then we'll aim to address these before the 1.0 RC.

RFC PRs

There have been numerous RFC PRs for different solutions to this problem. All of these have had useful and interesting parts and earlier RFCs have been heavily cannibalised by later ones. We believe that RFC PRs #245 and #250 are the most relevant and the eventual solution will come from these PRs and/or any ideas that emerge in the future. For a summary of some of the proposals and some discussion, see this discuss thread.

brendanzab commented 9 years ago

Comment moved to discuss.

why-jay commented 9 years ago

In December the Rust and Servo teams will all be in one place and we intend to make decisions on how to provide an efficient code reuse solution and plan the implementation in detail.

What ended up happening? :)

huonw commented 9 years ago

That paragraph is unfortunately outdated: we'd actually already postponed all discussion to after 1.0 since we believe any changes/features we will add are backwards compatible and there's a lot of more urgent (I.e. backwards incompatible) work that took priority.

why-jay commented 9 years ago

I understand. I'm excited to see what ends up being implemented.

breckinloggins commented 9 years ago

One thing I love about rust is that its non-free abstractions are usually explicit. I love knowing that a struct is "just a struct". It would be nice to say that features requiring runtime and fancy "under the hood" data representation support (for some value of fancy) are built from pluggable, orthogonal components.

So how about something like "anonymous composition is inheritance"? The idea would be that

struct Node {
    // stuff
}

struct FooNode {
   Node;
   // more stuff
}

implies a specific struct layout with the requisite syntactic sugar. For simplicity let's suppose that we only allow one anonymous component per struct.

EDIT: It was pointed out to me by @steveklabnik and others that rust has had trait objects since forever. I'll keep the following bit in place and just say that std::raw::TraitObject is the kind of thing I would love to be not only "pluggable" but "leave-out-able", in the sense that if I'm writing an OS and I can't implement that yet, I can tell the compiler that it can't make me any TraitObjects and that it wouldn't be ok for me to use them right now.

For dispatch, it would be nice if we could plug-in and compose the dispatch resolution mechanism. I don't want the "doesn't play well with others" feel of C++ vtables. What if I want to back this part of the language with the Objective-C runtime? Or Glib? The current "personality" of rust feels like this should be possible, in the same way that "give me your allocator function and then I'll let you use boxed things" works.

I guess my main point is that rust is the first language in a long time where I really feel like the modern features of the language don't come with being chained to a runtime layout and functionality set that's given from up on high by the Gods of Rust.

I would love it if the rust team could implement functionality like this while still retaining that ethos.

pcwalton commented 9 years ago

Virtual dispatch will of course be explicit under this proposal or any other.

steveklabnik commented 8 years ago

It's worth mentioning that @nikomatsakis has been working on this proposal: http://smallcultfollowing.com/babysteps/blog/2015/08/20/virtual-structs-part-3-bringing-enums-and-structs-together/

sighoya commented 6 years ago

What's the state in 2018?

Couldn't we agree to any "inheritance" proposal?

blueridanus commented 6 years ago

What happened to @nikomatsakis's proposal?

I'm trying to create a general UI framework for Rust, basically based on push-pull FRP. There's no way to do it, however, because there's no way to model a type hierarchy: enums don't actually "add" types together, and the nodes of enums aren't really types, only tags.

LifeIsStrange commented 5 years ago

Now that rust 2018 edition happened, shouldn't the priority be bumped up?

This is my main limiting factor and probably the last big remaining reason for outsiders to not learn rust...

moonheart08 commented 5 years ago

It's been five years. Isn't it about time this is un-postponed?

burdges commented 5 years ago

Inheritance results in unreadable buggy code, at least as inheritance is implemented in most languages. I think the original servo concerns were largely addressed, as all recent requests by servo team members were far more targeted, but I'm not entirely sure about the servo teams preferred techniques.

I think the most relevant concrete proposal is delegation https://github.com/rust-lang/rfcs/pull/2393 which dramatically simplifies delegating a trait to a field. And partial delegation gives you quite a close approximation to inheritance, while likely remaining explicit enough to reduce bugs. We could see delegation put on 2020s roadmap, but it remains rather complex, and doing ti wrong might break other priorities.

deepakggoel commented 4 years ago

I know that focus of Rust is on systems programming but business applications usually have lot of common state and without support for the issues mentioned here, are simply too awkward to implement.

As a former C++ programmer, I really got excited about all the neat things and elegant solutions provided by Rust, only to realize that I can’t use Rust because of lack of inheritance like abstraction. Adding this support will also ease rewrite of some of the Java based system software

Ixrec commented 4 years ago

My impression is that for most business applications, Rust's trait system already provides the encapsulation, abstraction and runtime polymorphism needed to satisfy all of the same use cases that traditional OOP inheritance systems do. And even in modern C++, class hierarchies are very rarely the best architectural choice.

It is true that you cannot mechanically transpile class-heavy C++/Java code directly to Rust, but "translating OOP patterns" to Rust is definitely a thing, and there's at least some documentation trying to address the difficulty: https://doc.rust-lang.org/book/ch17-03-oo-design-patterns.html Personally, I'm not aware of any useful OOP pattern whose practical code quality or abstraction benefits cannot be fully achieved in Rust via some other pattern (although transitioning an existing codebase from one to the other may be a difficult refactoring regardless). If you have something specific in mind, I'd recommend asking on https://users.rust-lang.org/ whether anyone's aware of a good Rust equivalent.


IIUC, the motivation for this issue is much more specific, and much more systems program-y: there are some layout and runtime efficiency guarantees that C++'s inheritance system provides, but Rust currently doesn't. AFAIK, they only matter in practice for use cases like web browser UI widget trees where a deep class hierarchy is both legitimately the right architecture and needs to be hyper-optimized (such that Java would've been a non-starter anyway). But those guarantees could be fully provided by Rust without adding traditional inheritance, most likely with things like "fields in traits" and a "prefix layout #[repr(...)] attribute" and so on.

wrl commented 4 years ago

I'm personally waiting for fields in traits to port some of my UI code over. However, it seems like the RFC repo hasn't been updated in about 2 years. Has it been abandoned?

burdges commented 4 years ago

I doubt fields in traits are abandoned per se, but it requires real design work, and they've many unfulfilled obligations around specialization, etc, so don't hold your breath.

We want fields in traits to resolve borrowing conflicts, not as some sugar for getters and setters, so some complexity arises from when and how do you promise disjointness among the fields. If you are not worried about borrowing disjoint fields, then all the disjointness promises would prevent you from doing what that getters and setters do.

There are more profitable issues, like:

I'd expect OO code would exploit dyn Trait heavily so that E0225 became an annoyance. We could either fix E0225 or else come up with good practices for combining supertraits.

You could imagine some proc macro #[derive(AnyTrait)] for trait reflection that somehow knows about other trait impls so T = dyn Trait works for many traits in

pub trait AnyTrait: 'static {
    fn downcast_ref<T>(&self) -> Option<&T>;
    fn downcast_mut<T>(&mut self) -> Option<&mut T>;
}

Could/should some Any variant include a lifetime?

pub trait Any1<'b>: 'b {
    fn type_id<'a: 'b>(&'a self) -> TypeId;
}
impl<'b> dyn Any1<'b> {
    pub fn is<T: Any1<'b>>(&self) -> bool { ... }
    pub fn downcast_ref<T: Any1<'b>>(&self) -> Option<&T> {
}
es50678 commented 4 years ago

I'm personally waiting for fields in traits to port some of my UI code over.

I second this.

While it's possible to do something like the following(where both Shape and Square implement Area):

trait Area{
    fn calculate_area(&self) -> f64;
}

struct Shape {
    area: f64,
    scale: i32,
}

struct Square {
    width: f64,
    shape: Shape
}

We could then call square.shape.area to grab the area, but to calculate the area we would only need to call square.calculate_area. It doesn't make sense, to me, to have a mechanism for inheriting methods but not fields. I'm seeing a lot of code duplication in my projects because of this, and that code duplication isn't necessary in Rust competitors(mainly, c++)

nielsle commented 4 years ago

@es50678 I think that you are asking for #2393. Also have a look at the Ambassador crate https://github.com/hobofan/ambassador

Serentty commented 4 years ago

I have a bad feeling that the only reason that this issue hasn't been closed is that it's coming from the Servo team, and that Rust will never actually add any of these things for ideological reasons. Sure, there's work that still needs to be done before this can be achieved, but no one seems to be working on it because it's “gross” OOP.

AndrewSav commented 4 years ago

Well, I think it's a pretty solid guess, given that the issue is 6 years old. If anyone (from the developer) had any interest in this being done, that would already had been indicated.

Serentty commented 4 years ago

I was in a salty mood when I wrote that earlier. I apologize for being annoying.

Ixrec commented 4 years ago

I think it's far more likely that issues on this repo just aren't curated that much. There's never really been a clear policy for when it makes sense to open or close an issue here, unlike on rust-lang/rust where inactive or obsolete issues seem to get closed regularly. Probably the only reason most of us even see these issues is because we're watching the repo for pull requests.

It would be interesting to hear from the Servo team whether they still care heavily about this. My personal impression from being over-attentive to the last several years of Rust design discussions is that "efficient single inheritance" represents only one set of layout optimization guarantees that real programs are interested in, and there are many other layout optimizations that matter, all of which can be achieved "manually" today (sometimes with unsafe, sometimes with safe wrapper types that have a bunch of bitshifting methods), so it's becoming difficult to argue these layouts in particular deserve more first-class support. But that's still just a vague impression I have.

Serentty commented 4 years ago

For sure, it's possible to do some of these things already through lots and lots of macros. If it were only Servo that needed this that would probably be the best solution. However, I see similar requests a lot from those working on GUIs.

I think it's far more likely that issues on this repo just aren't curated that much.

Yeah, I didn't mean to imply any ill intent. I was just in somewhat of a mood and I happened to remember this issue for some reason or another. It would probably be more productive for me to work on the mothballed fields in traits RFC or something like that.

matu3ba commented 4 years ago

@Serentty

  1. What exactly is the difference to well-named hygienic macros aside of evaluation order (penalizing non-usage) and no explicit syntax?
  2. Inheritance has a ton of edge-cases and the worst part about it is implicit state hiding from the user.
  3. Fundamentally this is about enforcing on a language-level not to have a million abstractions with the well-known problems by not making people rely on stateful (and very hard to refactor) abstraction.

And yes, I do get your point. Dynamic layout structure-changes are hard to model without inheritance, which is a fundamental tradeoff in data-driven approaches.

Serentty commented 4 years ago

I would argue that Rust conventions are well-established at this point. I don't think the Rust community would rush to use inheritance at every opportunity if they had it. The attitudes I see towards it are proof enough of that to me. Also, from what I can see pretty much any realistic inheritance proposal for Rust would be through traits, not through direct subtyping, as C++-style subtyping leads to all sorts of memory safety issues when objects get truncated. I think that the more roundabout nature of trait-based inheritance would largely offset the temptation to use inheritance where it isn't appropriate.

burdges commented 4 years ago

I'd think GUIs should obtain correctness and security from the lower layers, but then expose "sloppy but convenient" abstractions like inheritance to higher layers.

Servo's script::dom implements subtyping inheritance as required by JavaScript's DOM. Your preferred abstractions might differ if you're doing some native GUI or a game engine. It's true some GUIs like Qt encourage adoption by being JavaScript-like, but if you're doing that then maybe you should build directly upon Servo.


Inheritance distracts from real improvements, like delegation and fields-in-traits, but also..

We need trait specialization, not for being object orientated, but because it closes a performance gap with C++.

We need good toolbox crates for writing proc macros, so that proc macro code become semi-readable, which should improve all Rust DSTs, including more object oriented ones.

We might address E0225 so that dyn TraitA+TraitB becomes more usable, perhaps only if trait aliases exist like pub trait TraitAB = TraitA+TraitB, but perhaps via some "unnaned supertrait declaration" like pub trait _ = TraitA+TraitB;.

There is an extremely manual choice between monomorphisation vs trait objects, and Rust favors monomorphisation currently, so we should make trait objects more useful and ergonomic, develop patterns for being flexible about monomorphisation vs trait objects, and develop profiling tooling and practices that show when trait objects win.

We cannot afaik stabilize the current TraitObject type, partially because smart pointers could be larger than one usize. We could however provide some vtable access mechanism that handles smart pointers correctly like:

pub type VTablePointer = *mut ();

/// see https://github.com/rust-lang/rfcs/blob/master/text/1598-generic_associated_types.md
trait PointerFamily {
    type Pointer<T>: Deref<Target = T>;
}
trait PointerMutFamily : PointerFamily
where for<T> <Self as PointerFamily>::Pointer<T>: DerefMut { }

/// Access smart pointer's vtable for types like `&`, `&mut`, `Box`, `Rc`, `rc::Weak`, `Arc`, and `sync::Weak`, but not `Cell`, `RefCell`, `Mutex`, etc.  
unsafe trait PointerVTable : PointerFamily {
    /// Raw pointer to the vtable of a `Self::Pointer<T>` when `T = dyn Trait`.
    /// It normally returns the first aligned `*mut ()` after the pointer itself.
    /// If `T` is not `dyn Trait` then dereferencing this pointer is undefined behavior.
    fn vtable_offset<T>(ptr: Self::Pointer<T>) -> *mut VTablePointer;
}

I presume "cheap *casting" should really mean casting matrix among dyn Trait types, unlike Servo's DOM. If we can access the vtable pointer then doing this resembles:

/// Dynamic cast family
/// Identifies related related traits among whose `dyn Trait` types casts make sense.
pub trait DynCastFamily { }

/// Implemented by proc macros for `dyn Trait` types corresponding to traits within a dynamic cast family to which trait object casts occur. 
unsafe pub trait DynCastTo<CF: DynCastFamily> : ?Sized + 'static {
    const dyncast_index: usize;
}

/// Implemented by proc macros for real types within a dynamic cast family on which trait object casts occur.  Also a super trait for every trait within the dynamic cast family.
unsafe pub trait DynCastFrom<CF: DynCastFamily> : 'static {
    const fn dyncast_vtables(&self) -> &'static [VTablePointer];
}

impl<CF: DynCastFamily> dyn DynCastFrom<CF>> + 'static {
    fn dyncast<T,P>(mut self: P::Pointer<Self>) -> Option<P::Pointer<T>> 
    where T: DynCastTo<CF>, P: PointerFamily+PointerVTable,
    {
        let i = <T as CastTo<CF>>::dyncast_index;
        let new_vtable = self.dyncast_vtables()[i];
        if new_vtable.is_null() { return None; }
        let old_vtable = PointerVTable::vtable_offset(self);
        Some(unsafe {  *old_vtable.write(new_vtable);  mem::transmute(self)  })
    }
}

In short, we declare "casting families" of traits so that proc macros and build tooling construct a "casting matrix" that gives every type T within the casting family a slice of vtable pointers, and gives every dyn Trait for a trait within the family an index into all these slices. The vtable pointer is null if T: !Trait.

We'd expect high performance from this solution because it requires only two pointer dereferences from potentially well traversed tables and one if check. Arbitrary self types appear essential, but we do not require that PointerFamily ATC tricks, since separate dyncast_* methods for every self type work too, but they help with user defined smart pointers. We could achieve this with proc macros alone, and avoid build tooling, if we populate this casting matrix at runtime using lazy_static.

Serentty commented 4 years ago

I agree so strongly with everything you said there. Fields in traits are one of the features I find myself missing most in Rust. I don't care if one type can automatically inherit from another as long as I can abstract over types in terms of which fields they have. I think fields in traits are absolutely a good substitute for inheritance, while not necessarily encouraging people to use an inheritance-based design pattern.

Multiple trait objects are also something I've found myself running into and missing before.

fogti commented 4 years ago

Another interesting aspect is trait "object safety" and methods on dyn Traits, because they make working with "derived traits" (e.g. traits which have another trait as supertrait) a bit annoying, because traits can't be easily "upcasted". It is possible to work around this (I've done it in the as-any crate, even without many macros), but it starts to become annoying as soon as more traits are involved.

matu3ba commented 4 years ago

I do very much agree. Having the behavior explicit via macro_syntax/arguments would work.

We need trait specialization, not for being object orientated, but because it closes a performance gap with C++.

Could you shortly elaborate?

We need good toolbox crates for writing proc macros, so that proc macro code become semi-readable, which should improve all Rust DSTs, including more object oriented ones.

We might address E0225 so that dyn TraitA+TraitB becomes more usable, perhaps only if trait aliases exist like pub trait TraitAB = TraitA+TraitB, but perhaps via some "unnaned supertrait declaration" like pub trait _ = TraitA+TraitB;.

Would it make sense to enforce centralized type lookup on these (ie by cargo) or in a special file types.rs/traits.rs and have special macro syntax for aliasing aliased traits, if they are not in the local package/crate?

Ixrec commented 4 years ago

We need trait specialization, not for being object orientated, but because it closes a performance gap with C++.

Could you shortly elaborate?

The original RFC https://github.com/rust-lang/rfcs/blob/master/text/1210-impl-specialization.md is still a decent resource for this. Some details have changed, but it introduces all the important concepts, and provides clear and compelling examples.

Would it make sense to enforce centralized type lookup on these (ie by cargo) or in a special file types.rs/traits.rs and have special macro syntax for aliasing aliased traits, if they are not in the local package/crate?

Multi-trait objects have been covered extensively in many past threads, so we should probably avoid duplicating discussion. See https://github.com/rust-lang/rfcs/issues/2035 for example.

matu3ba commented 4 years ago

@Ixrec I did not find specific arguments or measurements about the current performance(as runtime) gap to c++.

Regarding to this I am missing some fundamentals to simplify (or I did not read properly enough the very long) in the quite complex rfcs:

  1. Every indirections(vtable lookup) has a price and ought to be clear to the programmer. (Dynamic dispatch)
  2. The other option is to drop the information how the object has been combined. (Monomorphization)
  3. Or you use the compiler to handle lifetimes,borrow checker etc with compile-time penalties and added compiler complexity. (Mix of both with anonymous enums/structs to track what is going on with the objects) (Visitor/Generator pattern) I may be wrong here or miss out something however.

This comes on top of the case handling how to combine the methods. What I am skeptical about @burdges approach 1. is the option to mutate the behavior via .super() calls in an non-trivial way to the user, because that is a form implicit state(not known to the caller). Other than that 1. is the easiest way to move forward to analyze the tradeoffs.

steveklabnik commented 4 years ago

To be clear, issues in this repository are basically just a scratch space for folks to talk. There's no guidelines about keeping them open or closed; there is no bearing on how relevant an issue is to Rust in any state of these issues. Keeping an issue open here doesn't mean the team is considering the feature, and closing an issue does not mean the team has rejected the feature. It is not part of the decision making process in any way.

HydrogenC commented 4 years ago

I think that class member inheritence as well as upcasting is quite necessary.

ZhangHanDong commented 3 years ago

@ibraheemdev Rust has a Rust Style.

ibraheemdev commented 3 years ago

Rust favours composition over inheritance. Ok. So we do not want method overriding or other subtyping features. But why is there not an easy way to compose structs like this?


struct Animal {
  name: String
}

impl Animal {
  fn print_name(&self) {
    println!("{}", self.name)
  }
}

struct Dog {
  owner: String,
  ..Animal
}

fn main() {
  let dog = Dog { owner: "John", name: "Ripley" }
  dog.print_name();
}
burdges commented 3 years ago

There is discussion of delegation in the language in #2393 and discussions of delgation proc macros like delegate linked from https://www.reddit.com/r/rust/comments/ccqucx/delegation_macro/etpfud5/?utm_source=reddit&utm_medium=web2x&context=3

aldanor commented 3 years ago

@ibraheemdev

But why is there not an easy way to compose structs like this?

Well, if you insist on composition...

struct Animal {
    pub name: String,
}

impl Animal {
    pub fn print_name(&self) {
        println!("{}", self.name);
    }
}

struct Dog {
    owner: String,
    animal: Animal,
}

impl std::ops::Deref for Dog {
    type Target = Animal;

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

fn main() {
    let dog = Dog { 
        owner: "John".into(), 
        animal: Animal { name: "Ripley".into() },
    };
    dog.print_name();
    println!("{}, {}", dog.name, dog.owner);
}
ibraheemdev commented 3 years ago

@aldanor Using Deref is quite limited and is widely regarded as a bad practice. This is not a solution to composition. My point is that because Rust prefers composition over inheritance it should have first class language constructs to aid in composition and efficient code reuse.

ssokolow commented 3 years ago

From what I remember, last it was discussed at length, the result was "Desirable, but not as easy as it sounds and we have things that are needed more urgently, like async/await, to get implemented first".

(i.e. There's no single objectively superior way to implement a code reuse mechanism, so more discussion is needed to decide which trade-offs are best to enshrine in the core language as the blessed primary way to do it.)

burdges commented 3 years ago

There was a consensus that delegation should be explored outside rustc first, ala delegate. We've enough plausible delegation patterns that practical exploration helps. I suppose doing delegation via proc macros could expose more proc macro shortcomings too, which then improves proc macros and better supports whatever complex delegation scenarios rustc excludes.

I frequently flip the type structure wherever I'd otherwise be tempted by simple local inheritance:

struct MuSigInner { .. }
pub struct MuSigCommitStage { inner: MuSigInner, .. }
pub struct MuSigRevealStage { inner: MuSigInner, .. }
pub struct MuSigSignStage { inner: MuSigInner, .. }

vs

pub struct MuSig<Stage> { .. }
pub struct CommitStage { .. }
pub struct RevealStage { .. }
pub struct SignStage { .. }

Any methods you want inherited from MuSigInner in the first simply become generic methods in impl<Stage> MuSig<Stage> in the second. In this way, all your methods become more explicit about their provenance and role, which avoids inheritance bugs and confusion.

dbsxdbsx commented 3 years ago

@ibraheemdev

But why is there not an easy way to compose structs like this?

Well, if you insist on composition...

struct Animal {
    pub name: String,
}

impl Animal {
    pub fn print_name(&self) {
        println!("{}", self.name);
    }
}

struct Dog {
    owner: String,
    animal: Animal,
}

impl std::ops::Deref for Dog {
    type Target = Animal;

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

fn main() {
    let dog = Dog { 
        owner: "John".into(), 
        animal: Animal { name: "Ripley".into() },
    };
    dog.print_name();
    println!("{}, {}", dog.name, dog.owner);
}

I think this way is the best workaround. But still, that working out this issue by adding field in trait would be more consistent with the design of rust.

Serentty commented 3 years ago

I wonder if properties (which would be allowed in traits) would be a good option. I'm quite fond of them, but I've heard opposition to them for being too magical. I disagree, but oh well.

DanielJoyce commented 2 years ago

What if Animal is defined in another crate, and Dog is defined in your crate. And some point down the line, Animal adds a field Dog already has. I think this gets into the same kind of thinking around the current orphan rules and coherence.

Iron-E commented 2 years ago

@dbsxdbsx @ibraheemdev I personally would implement that like this:

struct Animal {
    pub name: String,
}

trait AnimalExt {
  fn get_animal(&self) -> &Animal;
  fn get_name(&self) -> &str {
    &self.get_animal().name
  }
}

impl AnimalExt for Animal {
  fn get_animal(&self) -> &Animal {
    self
  }
}

struct Dog {
    owner: String,
    animal: Animal,
}

impl AnimalExt for Dog {
  fn get_animal(&self) -> &Animal {
    &self.animal
  }
}

fn main() {
  let dog = Dog { 
    owner: "John".into(), 
    animal: Animal { name: "Ripley".into() }
  };
  println!("{}", dog.get_name());
  println!("{}, {}", dog.get_name(), dog.owner);
}

I think it is more scalable for the number of methods and does not abuse Deref— you can make a method that accepts impl AnimalExt as a parameter and it will be valid for both Animal and Dog.

SOF3 commented 2 years ago

While many people suggest abusing Deref for code reuse, it is actually both ergonomic and idiomatic to use AsRef for a similar purpose:

fn feed(mut animal: impl AsMut<Animal>) {
    animal.as_mut().food += 1;
}

struct Dog {
    animal: Animal,
    other_fields: Xxx,
}

impl AsMut<Animal> for Dog {
    fn as_mut(&mut self) -> &mut Animal { &mut self.animal }
}

let dog: Dog = unimplemented!();

feed(&mut dog);

This problem is already well-solved with std::convert traits. All it takes is a slight change in the method parameter signature. Why need so much complexity?

mrahhal commented 2 years ago

@SOF3 This is a nice none-hacky approach but how do you solve wanting to match on a base type and do different things depending on the sub type?

let base = get_some_subtype();

match base {
  SubType1 => ...
  SubType2 => ...
}

This is why I want code reuse in the first place. I want to be able to fully access info about both the base and sub types, especially to match on base types and do branching logic (no matter whether that is implemented through inheritance or composition).

SOF3 commented 2 years ago

@SOF3 This is a nice none-hacky approach but how do you solve wanting to match on a base type and do different things depending on the sub type?

let base = get_some_subtype();

match base {
  SubType1 => ...
  SubType2 => ...
}

This is why I want code reuse in the first place. I want to be able to fully access info about both the base and sub types, especially to match on base types and do branching logic (no matter whether that is implemented through inheritance or composition).

What is the type of base? You can't own a dynamically sized type. You need an enum for that, which we already can match.

mrahhal commented 2 years ago

By matching on a ref (by having a discriminant of some kind similar to enums) in some way maybe. The snippet above isn't rust sanitized and haven't thought it through much. What I mean is I've been wanting a way to achieve that as a result (not through enums, but through inherited/composited types), disregarding the method/syntax, and we don't have that yet.

SOF3 commented 2 years ago

By matching on a ref (by having a discriminant of some kind similar to enums) in some way maybe. The snippet above isn't rust sanitized and haven't thought it through much. What I mean is I've been wanting a way to achieve that as a result (not through enums, but through inherited/composited types), disregarding the method/syntax, and we don't have that yet.

that is specialization, which is an antipattern. why not use enums for that? alternatively, make it a method in Animal/AminalExt?

although unidiomatic, this can also be achieved through Any::downcast_mut() in an if-let-else-if-let chain.

mrahhal commented 2 years ago

Enums don't really solve all cases relating to this. Just look at how rustc tries really hard to emulate inheritance by using enums for the different nodes in the tree (and one of the members talked exactly about this case when they were discussing specialization in the past). Enums just are not always the answer. Whether it's called specialization or something else, rust is in serious need for a better way to compose shared code, is what I'm saying. Some workarounds work for the simpler cases (enums, deref, AsRef), but they're not nearly enough.