Best-Practices for Extending Existing Struct Fields

Hello All,

I'm making an application where i'm hoping to support extensions (from feature flags, to other rust crates, or even other languages if possible).

Specifically, I have a struct named Task which is implemented with the most bare-bones fields imaginable {priority, name, completed status, id}. I don't want to overload my core struct because it is important that this struct is modular, and that people can add and remove fields as they need.

I know that Traits can't implement fields, and while I know the rust-favored answer is composition

Ex:

struct extended_task {
    task: Task,
    extra_field: ...
}

this creates major implications for any package that uses the core Task, but would like to leverage these new fields, I would either need to wrap everything in a Box<>, or deal with ever-growing switch statements to accomidate the growing number of structs.

  • in addition, this makes the idea of inter-struct composition feel difficult to impossible (what is we have extended_task_a and extended_task_b? does the user have to choose? what if I want BOTH?)

I'm struggling to see a situation that doesn't lead to an explosion of types, and therefore, type checking within client code.

how can I make something where a struct can be extended, and allow this improvement to translate to various platforms for the code? (cli, web, gui, etc...)

As always, thank you for taking the time to read this and for your expertise

How about making Task generic?

// Ext = () means "no extra data"
struct Task<Ext = ()> {
    priority: u32,
    name: String,
    completed: bool,
    id: u64,
    extra: Ext,
}

Then the functionality within your crate can work with Task<Ext> for any Ext, or perhaps any Ext implementing some trait TaskExtra that you make up. If you need to store tasks with different types of extra data together, you can convert them all into type DynTask = Task<Box<dyn TaskExtra>> and then downcast when it's time to return them to the caller.

[ Edit: here's an example of downcasting since it's kind of tricky to implement and I always forget how it goes: Rust Playground. ]

A "lightweight" alternative would be to use u64 or something for the extra field, and require the caller to provide its own interpretation of that integer, for example by using it to index into an auxiliary table where the real extra data is stored.

3 Likes

This is somewhat unrelated. But I was also having a similar problem for a different use case. I was facing the problem of switch case explosion to deal with ever increasing number of enums and structs with different meaning attached to them. I ended up with treating everything as trait objects. And once I discovered what patterns were emerging, I made the trait objects generic. @cole-miller 's approach seems like a nice way of doing things, I would have to think if it would also help me improve my expression composition.

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.