Search code examples
ooprustparadigms

How to avoid code repetition in rust (in struct, and traits)?


This is the Trait example from the rust by example book

struct Sheep { naked: bool, noise: &'static str }

trait Animal {
    // Associated function signature; `Self` refers to the implementor type.
    fn new(name: &'static str) -> Self; 

    // Method signatures; these will return a string.
    fn name(&self) -> &'static str;
    fn noise(&self) -> &'static str;

    // Traits can provide default method definitions.
    fn talk(&self) {
        println!("{} says {}", self.name(), self.noise());
    }
}

impl Sheep {
    fn is_naked(&self) -> bool {
        self.naked
    }

    fn shear(&mut self) {
        if self.is_naked() {
            // Implementor methods can use the implementor's trait methods.
            println!("{} is already naked...", self.name());
        } else {
            println!("{} gets a haircut!", self.name);

            self.naked = true;
        }
    }
}

// Implement the `Animal` trait for `Sheep`.
impl Animal for Sheep {
    // `Self` is the implementor type: `Sheep`.
    fn new(name: &'static str) -> Sheep {
        Sheep { name: name, naked: false }
    }

    fn name(&self) -> &'static str {
        self.name
    }

    fn noise(&self) -> &'static str {
        if self.is_naked() {
            "baaaaah?"
        } else {
            "baaaaah!"
        }
    }
    
    // Default trait methods can be overridden.
    fn talk(&self) {
        // For example, we can add some quiet contemplation.
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
}

fn main() {
    // Type annotation is necessary in this case.
    let mut dolly: Sheep = Animal::new("Dolly");
    // TODO ^ Try removing the type annotations.

    dolly.talk();
    dolly.shear();
    dolly.talk();
}

Let's say I want to add a other animal, for example a cat. A cat can't be naked, but has a name, and makes some noise.

struct Cat { name: &'static str }

impl Animal for Cat {
    // `Self` is the implementor type: `Sheep`.
    fn new(name: &'static str) -> Cat {
        Cat { name: name }
    }

    fn name(&self) -> &'static str {
        self.name
    }

    fn noise(&self) -> &'static str {
        "meow."
    }

    fn talk(&self) {
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
}

Here I implemented the exact same name() function twice. That's not a problem for this tiny function, but what if both Sheep and Cat implemented a long and complex function, that was using data from their instance (&self) ? Should I copy/paste the same function in the two impl blocks ? First I looked to traits, that seems to be a good way to avoid code repetition.

trait Name {
   fn name(&self) -> &'static str {
       self.name  
   }
}

impl Name for Sheep {}
impl Name for Cat {}

But that didn't worked, because this trait could be implemented on a struct that doesn't have a name field. no field 'name' on type '&mut Self'

So my question is: how should I organise my code in order to avoid repetition of method's implementations on different structs, for methods that have access to a mutable reference of their structs?


Solution

  • When you implement a trait, you need to implement everything of it. You're probably looking for some mechanism other languages call abstract classes. We don't have that in Rust (for many reasons). You can always move complex code outside and reuse it.

    trait Animal {
        fn new(name: &'static str) -> Self;
    
        fn name(&self) -> &'static str;
        fn talk(&self);
    }
    
    struct Cow {
        age: Option<u32>,
        name: &'static str,
    }
    
    struct Sheep {
        name: &'static str,
    }
    
    impl Animal for Sheep {
        fn new(name: &'static str) -> Self {
            Self { name }
        }
    
        fn name(&self) -> &'static str {
            self.name
        }
    
        fn talk(&self) {
            complex(self, "baaahh");
        }
    }
    
    impl Cow {
        fn set_age(&mut self, age: u32) {
            self.age = Some(age);
        }
    }
    
    impl Animal for Cow {
        fn new(name: &'static str) -> Self {
            Self { age: None, name }
        }
    
        fn name(&self) -> &'static str {
            self.name
        }
    
        fn talk(&self) {
            if let Some(age) = &self.age {
                complex(self, "moooooo");
            } else {
                complex(self, "MOOOOOOO!!!!");
            }
        }
    }
    
    fn complex<A>(animal: &A, noise: &'static str)
    where
        A: Animal,
    {
        println!("Animal {} makes noise {}", animal.name(), noise);
    }
    
    fn animal<A>(name: &'static str) -> A
    where
        A: Animal,
    {
        A::new(name)
    }
    
    fn main() {
        let dolly: Sheep = animal("Dolly");
        let mut lolly: Cow = animal("Lolly");
    
        dolly.talk();
        lolly.talk();
    
        lolly.set_age(5);
        lolly.talk();
    }
    

    In case of a few structs you should really just duplicate code. If you're planning to implement 50 structs, then maybe create a macro for this

    impl AudioNode for Echo {
        default_audionode_impl!()
    
        fn process(&mut self) {
            // ..
        }
    }