Inherited Traits / Implementations (Methods)?

I know this probably gets asked a lot, but I am looking for a way to use some inheritance in Rust. I don't quite yet understand how to practically design with traits / implementations. I'm still interested in comparing its methodologies. In other words, what patterns or use cases does it intend for?

First of all, I love Rust. I've studied it quite a bit. I want to see it go places, and I agree and am impressed with the philosophy overall. I know at one point there was virtual inheritance features that have since been removed. I don't, however, believe in going so far as to say composition-over-inheritance means no inheritance at all. At least the principle behind "copy-on-write" belongs in some form, and liberates proper composition. I trust Rust ultimately will integrate something worthwhile.

Anyway, I just want to be able to have a tree of elements, each with their own methods (which are inherited from multiple layers.) I am experimenting now -- just not sure about the inheritance part.

I've seen syntax like this.

trait B : A { fn b(&self); }

but don't know what it does.

So, I have elements that need to have:

  1. methods of the same name with different behavior,
  2. which are inherited from multiple layers,
  3. but they also need to have some unique methods.
trait B : A { fn b(&self); }

is what we call a supertrait. It's basically a trait bound on a trait. The book explains that here.

You want these elements with different behaviors in the same tree?

Anyway, I just want to be able to have a tree of elements, each with their own methods (which are inherited from multiple layers.) I am experimenting now -- just not sure about the inheritance part.

So, I have elements that need to have:

   1. methods of the same name with different behavior,
   2. which are inherited from multiple layers,
   3. but they also need to have some unique methods.

Your problem here falls in 2 categories:

  1. you want dynamic dispatch
  2. you want an heterogeneous tree

Both don't quite need inheritance, but you will most likely need conversions.

The syntax you describe is similar to inheritance: traits can request other traits to be implemented by expressing a bound on the type they are implemented on. This allow you to use methods of these traits within the implementation.

Dynamic dispatch, you can get in two ways:

If you have a bounded set of alternatives, I generally recommend a huge enum:

enum Node {
    Root(RootStruct),
    Branch(Vec<Node>),
    Leaf(LeafEnum),
    Bird(Seeds)
}

enum LeafEnum {
    Type1,
    Type2,
    Type3
}

(Because trees are strictly better with birds in it). This gives you a lot of convenience because then you figure out the exact type you are handling at any point in the code by matching on these two enums.

If you want the tree fully dynamic, you can always reach for trait objects.

The issue with trait objects is that they can only point to one trait. But you can work around that by having a trait that works as a landing pad and allows optional conversion into any other potentially implemented interfaces. See an example here:

trait ExtendedLeafNode : LeafNode {
    fn foo(&self);
}

trait LeafNode {
    fn as_extended_leaf_node(&self) -> Option<&ExtendedLeafNode>;
}

struct NotExtended;

impl LeafNode for NotExtended {
    fn as_extended_leaf_node(&self) -> Option<&ExtendedLeafNode> {
        None
    }
}

struct Extended;

impl LeafNode for Extended {
    fn as_extended_leaf_node(&self) -> Option<&ExtendedLeafNode> {
        Some(&*self as &ExtendedLeafNode)
    }
}

impl ExtendedLeafNode for Extended {
    fn foo(&self) {
        println!("called!");
    }
}


fn main() {
    let node = NotExtended;
    call_foo(&node);
    let extended = Extended;
    call_foo(&extended);
}

fn call_foo(node: &LeafNode) {
    if let Some(extended) = node.as_extended_leaf_node() {
        println!("here");
        extended.foo()
    }
}

I hope this clarifies a few things.

6 Likes

I think the "landing pad" technique should be mentioned in the book, which I don't think it was in the first edition; admittedly, I've not checked the second. So if it's still not present there, it would be helpful to talk about it there.

2 Likes

In terms of good practical design it can be better to not necessary use a one size fits all approach eg landing pad or Any.

When you design contains a structural component that takes external elements; first choice is between monomorphic (using of template of a trait) or polymorphic (using trait object.) The trait should be limited to what behaviour is used for the structure.
If you need the element elsewhere at the same time use reference counting. Basic case you can write impl TheTrait for Rc<RefCell<RealElement>> and make all the function forwarding to the internal element. Or use another struct composed of the reference counter. Or anyway you wish, since it is all hidden inside the trait. (Free to make code changes internally without modifying/breaking the interface the trait provides.)

1 Like

To me, it sounds like what you want is composition + delegation, and unfortunately delegation requires a bunch of explicit boilerplate for each method you want to delegate (or some macro fanciness) currently. There are talks about adding better support for delegation someday, though.

I'm curious, have you seen the OOP chapter in the second edition of the book? Was it helpful or still missing some examples/concepts?

1 Like

Yeah, that was very well written. I've been going through Rust By Example and the Book 2nd Edition page by page and enjoying the heck out of it. Thanks everyone for chiming in!

The delegation/landing pad is the first idea that came to mind while experimenting, so it's intuitive enough. While that does lock programs into certain composition and thus patterns, it happens to be exactly how I had already designed my program. :slight_smile: It's not a bad thing to have a strict paradigm, which the principles of I find to be very true here, since ultimately it enables efficiency, robustness, and scale.

I'm coming from JavaScript, where patterns based on dynamic memory allocation, especially with objects, are common. It's a whole lot more high-level organization and flow, really to the point of just being fundamental. It's great to see a middle-ground that still favors performance. I'd be very curious to study the performance of these delegation patterns, since I do not know much about compilers, and Rust is an interesting case.

That being said, I may have a good example that could go in the book. I am designing a modular browser for the desktop environment, a sort of GUI toolkit for web developers. (does not work with regular web pages.)

Using delegation, every element has a Model, as well as Controller and View component, which are derived from a hierarchy. So, for a Controller, a submit button would derive from Element->Form->Button->Submit, and each layer adds and modifies existing API, such as ".submit()" or ".mouseup()". The View does something similar, and may derive from Element->Mesh->Polygon->Rectangle to specify shader code such as "background-color".

Delegating to Controller/View components, and Implementing Traits can do this form perfectly, but to achieve the hierarchy, there will have to be lots of redundancy, which maybe involving Bound Traits could at least make a bit easier, although not much. I will continue to experiment and post some code examples later if you like. Still hoping to come to a nice solution, since I think hierarchy makes the most sense here, but we'll see.

Is this landing pad trait pattern still possible on todays rust? I always seem to get an error that as can only be used to cast primitive types?

Is there an alternative for going from one trait to another? I'm looking into query_interface, but that seems to use as as well in it's readme and require to have a Box<ConcreteType> as a starting point.

This still does work, (with the added benefit that we can now add the dyn keyword for enhanced readability), as long as you are casting behind some kind of pointer:

If expr: T and T : Trait,

  • &expr as &dyn Trait,

    • giving access to &self methods,
  • &mut expr as &mut dyn Trait,

    • giving access to &self and &mut self methods,
  • Box::new(expr) as Box<dyn Trait>,

    • owned, and giving access to &self, &mut self and self: Box<Self> methods,
  • Rc::new(expr) as Rc<dyn Trait>,

    • owned, and giving access to &self, and self: Rc<Self> methods,
  • etc.

See the reference (no pun intended :stuck_out_tongue:)

4 Likes

@Yandros Thanks for pointing that out. I tend to sometimes forget the existence of the reference and the nomicon, which also has a section on type coercions... Suppose I need to liberate a day or two to study the less obvious parts of the typesystem...