Search code examples
genericsrusttraits

Rust Generics with Default Trait Implementations


I'm working on a Rust project where I have defined several traits (A1, A2, and A3) with multiple implementations for each trait. I also have a struct Algo that takes generics based on these traits. I want to provide default trait implementations for the generics in case no specific implementation is provided by the user.

Here's my code:

pub trait A1 {
    fn test1();
}

#[derive(Default)]
pub struct Ex1_A1;

impl A1 for Ex1_A1 {
    fn test1() {
        println!("Ex1_A1");
    }
}

pub struct Ex2_A1;
impl A1 for Ex2_A1 {
    fn test1() {
        println!("Ex2_A1");
    }
}

pub trait A2 {
    fn test2();
}

pub struct Ex1_A2;
impl A2 for Ex1_A2 {
    fn test2() {
        println!("Ex1_A2");
    }
}

pub trait A3 {
    fn test3();
}

pub struct Ex1_A3;
impl A3 for Ex1_A3 {
    fn test3() {
        println!("Ex1_A3");
    }
}

struct Algo<B: A1, S: A2, M: A3> {
    a1: B,
    a2: S,
    a3: M
} 

impl <B: A1, S: A2, M: A3> Algo<B, S, M> {
    pub fn new(bbs: Option<B>, su: Option<S>, mac: Option<M>) -> Self {
        let bbs = bbs.unwrap_or(Ex1_A1);
        let su = su.unwrap_or(Ex1_A2);
        let mac = mac.unwrap_or(Ex1_A3);
        Self {
            a1: bbs,
            a2: su,
            a3: mac,
        }
    }
}

My question is, how can I provide default implementations for the generics B, S, and M in the Algo struct? I've tried using Option and unwrap_or, but it doesn't seem to work as expected.

I'd appreciate any guidance or examples on how to handle this scenario effectively in Rust.

Thank you!


Solution

  • Providing the default doesn't mesh well with static dispatch because the generic implementation and default implementation don't have the same type, so fields of Algo can't be initialized with an unambiguous type.

    A straightforward way to resolve that is to type-erase Algo fields using dyn .... Although dynamic dispatch has a bad rap due to the indirection it introduces, it's really perfectly fine when used as gateway into the algorithm. For example, you can define Algo like this:

    struct Algo {
        a1: Box<dyn A1>,
        a2: Box<dyn A2>,
        a3: Box<dyn A3>,
    }
    
    impl Algo {
        pub fn new(
            bbs: Option<Box<dyn A1>>,
            su: Option<Box<dyn A2>>,
            mac: Option<Box<dyn A3>>,
        ) -> Self {
            Self {
                a1: bbs.unwrap_or_else(|| Box::new(Ex1_A1)),
                a2: su.unwrap_or_else(|| Box::new(Ex1_A2)),
                a3: mac.unwrap_or_else(|| Box::new(Ex1_A3)),
            }
        }
    }
    

    For this to work, your trait methods need to take &self:

    pub trait A1 {
        fn test1(&self);
    }
    
    #[derive(Default)]
    pub struct Ex1_A1;
    
    impl A1 for Ex1_A1 {
        fn test1(&self) {
            println!("Ex1_A1");
        }
    }
    // ...
    

    Finally, new() is now invoked with trait objects:

    fn main() {
        let algo2 = Algo::new(Some(Box::new(Ex1_A1)), None, None);
    }
    

    Playground