Don't duplicate common properties

I am sure this is a question that has been answered multiple times, but I cannot find a straightforward answer anywhere. If this has been answered, a simple link would be great. Thanks for the patience!

I have two unrelated structs, call them Cat and Dog. And I have a couple common traits that define shared behavior.

Some of that shared behavior are a list of state properties (fur_color, eye_color, etc).

I have implemented the traits for each Cat and Dog, but then I have to copy and paste the various state variables. It's a minor inconvenience, but I can't help but feeling like I'm missing something. Like I am thinking about this is a non-rusty way.

I know there is no direct inheritance (and I love the way traits are so powerful), but is there a way to avoid having to copy and paste all my properties alongside the traits?

Ah, the answer seems to be derive macros: Procedural Macros - The Rust Reference

It's hard to answer when your example is just about dogs and cats, and not about the real thing you are trying to do.

1 Like

You definitely don't need to go as far as derive macros. Regular macros are probably more what you're looking for to implement some common items with repetitive code structure.

Here's really basic sample which might point you in the right direction:

macro_rules! define_animal { ($name:ident) => {
pub struct $name {
    fur_color: String,
    eye_color: String,
}
} }
define_animal!(Dog);
define_animal!(Cat);

Also, as your focus seems to be on adding standard fields, you might check out this post on implementing custom macros for creating more complex structs.

1 Like

How about adding a third struct having the common properties?

struct PetProps {
  fur_color: some_type,
  eye_color: another_type,
}

struct Cat {
  props: PetProps,
  ...
}

struct Dog {
  props: PetProps,
  ...
}

This way you can not just add common fields to both Dog and Cat but also common functionality via use of:

impl PetProps {
   ...
}

You can then access the shared props via:

let d = Dog::new();
d.props.fur_color
6 Likes

Generics and enums are two more tools you may find handy here. Here are some other recent-ish threads that may also help:

Thank you all for the amazing feedback. This community is easily the most inviting in technology.

I recognize that I should have given a more full example. I just didn't want to bog it down. For posterity, here is the solution I ended up going with.

I have multiple entities, Game, Playable, Peripheral and others. They are defined types that describe real world things. And that description is based on the Web of Things standard emerging from mozilla and others: Web of Things (WoT) Thing Description.

So, each of these entities has a common set of "meta" data such as title, description, etc.

What I ended up doing was to delegate those common pieces to an independent struct:

struct Meta {
     title: String,
     description: String,
     // ...
}

I then created a HasMetaTrait with appropriate getters and default implementations:

trait HasMeta {
     fn meta() -> Meta;
     fn title(&self) -> &String {
          &self.meta().title
     }

     fn description(&self) -> &String {
          &self.meta().description
     }
}

Which them means all I have to do on each type is implement a meta() method and property. In reality there are 20 or so meta fields and I just wasn't excited about copying those across 10 types. This is an elegant solution.

The only concern was json serialization, but it appears that serde can delegate to methods for serialization so I can keep those meta properties "top level" instead of serializing the meta object itself.

I'd be open to a more rust-idiomatic way of doing this, but this is going work fin. I appreciate everyone's feedback.

1 Like

You may find this talk useful:

4 Likes

That was super useful, yeah.

One small note - if I follow correctly, the struct you are implementing HasMeta on will have the Meta value in a known field, so you'd need &self.meta.title, &self.meta.description, etc. Otherwise you could just follow on the known fn meta(&self) -> Meta and go with &self.meta().description so that accessing a struct's meta field is guaranteed, even if the field moves around.

Also, at this point you can definitely use a macro to implement HasMeta on all the structs you want at once.

And one other thought, since you have HasMeta with the fn meta(&self) -> Meta function, you could make MetaGetters as a second, separate trait which requires HasMeta. That allows some flexibility in terms of separating implementations, working with macros, etc.

Here's a playground with several of these concepts worked in.

1 Like

@alice that was a great talk. I appreciate it. Illuminating.
@trentj great resources. Especially the first one was helpful.

I think in general my mixup was that these types also have json representations that need to be very exact. And, I was trying to shoe-horn my rust definitions to match 1-for-1 the json definitions. But that's not needed. Serde can be made to do magic and serialize and deserialize from the spec format while allowing me to build the definitions in a much more rust-y way.

1 Like

Splitting the getters and setters is a fantastic idea.

And you are right about my example. I typed it fast and in the actual textbox so I didn't review. I've corrected it. Gratsi.

I really enjoyed that talk. @alice you always have such interesting and helpful Rust videos!

That got really quickly to a very important issue in learning Rust that I feel is still under-served in books and other content (though this forum is great for it). Conceptually Rust syntax is not too challenging, at least relative to other languages. But learning to work with Rust to do things smarter (and avoiding pains associated with bad patterns, especially on larger projects) can be very challenging. I think it's also the line beyond which Rust can start to feel like it's really opening up in terms of "usability".

I guess I wasn't really going anywhere with that train of thought... maybe I'm just fishing for more good Rust resources along those lines :rofl:.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.