15

I have a rust trait which is supposed to add a value to vector. In order for the add_job function to be working, it must be made sure that the vector exists when the trait is implemented for a concrete struct.

The following code fails of course, because jobs is never implemented. It's just there to demonstrate my intention:

trait Person {
    // default implementation of add job
    fn add_job(&self, job: String) {
        self.jobs.push(job)
    }
}

struct Customer {
    // add_job is used as default implementation 
    // provided by trait
}

impl Person for Customer {
    // some stuff
}

fn main() {
    let mut george = Customer {};
    george.add_job("programmer".to_string());
}

Is there a way to have a trait which also provides struct members?

Propably not, but what would be the "rustful" way to solve the above problem?

1 Answer 1

21

Traits can't provide or require struct fields. Though there is an RFC (#1546) about allowing fields in traits. However, there isn't any unstable features allowing this (yet?).


You can still simplify what you're trying to do though. I've taken the liberty to rename and change your trait, to be able to provide more thorough examples.

Let's consider that we have a Jobs trait. Which defines various methods, that all requires the jobs: Vec<String> field.

trait Jobs {
    fn add_job(&mut self, job: String);
    fn clear_jobs(&mut self);
    fn count_jobs(&self) -> usize;
}

Using a macro

One solution could be to use a macro, which implements all those methods.

macro_rules! impl_jobs_with_field {
    ($($t:ty),+ $(,)?) => ($(
        impl Jobs for $t {
            fn add_job(&mut self, job: String) {
                self.jobs.push(job);
            }

            fn clear_jobs(&mut self) {
                self.jobs.clear();
            }

            fn count_jobs(&self) -> usize {
                self.jobs.len()
            }
        }
    )+)
}

Then you can easily reuse the code, by using the macro.

struct Person {
    jobs: Vec<String>,
}

struct Customer {
    jobs: Vec<String>,
}

impl_jobs_with_field!(Person);
impl_jobs_with_field!(Customer);
// or
impl_jobs_with_field!(Person, Customer);

Using a second HasJobs trait

Another solution could be to introduce a second HasJobs trait. Then you can use a blanket implementation for Jobs if a type implements HasJobs.

trait HasJobs {
    fn jobs(&self) -> &[String];
    fn jobs_mut(&mut self) -> &mut Vec<String>;
}

impl<T: HasJobs> Jobs for T {
    fn add_job(&mut self, job: String) {
        self.jobs_mut().push(job);
    }

    fn clear_jobs(&mut self) {
        self.jobs_mut().clear();
    }

    fn count_jobs(&self) -> usize {
        self.jobs().len()
    }
}

Now HasJobs still needs to be implemented for all your types. But if Jobs has a significant amount of methods. Then implementing HasJobs is a lot easier to deal with. Which we can also do using a macro:

macro_rules! impl_has_jobs {
    ($($t:ty),+ $(,)?) => ($(
        impl HasJobs for $t {
            fn jobs(&self) -> &[String] {
                &self.jobs
            }

            fn jobs_mut(&mut self) -> &mut Vec<String> {
                &mut self.jobs
            }
        }
    )+)
}

Then once again, you just do:

struct Person {
    jobs: Vec<String>,
}

struct Customer {
    jobs: Vec<String>,
}

impl_has_jobs!(Person);
impl_has_jobs!(Customer);
// or
impl_has_jobs!(Person, Customer);

Using Deref and DerefMut

Lastly, if Customer is always a Person, then you could change Person into a struct and use composition, i.e. add person: Person to Customer (and other types).

So first, impl Jobs for Person:

struct Person {
    jobs: Vec<String>,
}

impl Jobs for Person {
    fn add_job(&mut self, job: String) {
        self.jobs.push(job);
    }

    fn clear_jobs(&mut self) {
        self.jobs.clear();
    }

    fn count_jobs(&self) -> usize {
        self.jobs.len()
    }
}

Then now you can use Deref and DerefMut to dereference Customer to Person. Thus all Person's methods are available directly through Customer.

However, this solution only works if you only have one "trait", i.e. you can only Deref Customer to Person. You wouldn't be able to also Deref Customer to SomethingElse.

use std::ops::{Deref, DerefMut};

struct Customer {
    person: Person,
}

impl Deref for Customer {
    type Target = Person;

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

impl DerefMut for Customer {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.person
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

As a comment, there are some RFCs to allow fields on traits
I was already looking for it. But thanks for mentioning anyways! :)
Great Answer, more than I had hoped for!
Thanks for this answer because of its references! Been coding in Rust for at least a year now, coding it in production as well, but I just found out what Blanket Implementations are. Keep seeing it in documentation, it makes me understand how a type functions more, but no idea how to implement it myself. And IT IS AMAZING KNOWING ABOUT THIS. Will refactor a lot of my codes.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.