Search code examples
methodsrustdefaulttraits

How can I use the same default implementation for this Rust trait


I want to implement a trait that allows assigning generic types. So far I have tested this for u32 and String types:

trait Test {
    fn test(&self, input: &str) -> Self;
}

impl Test for String {
    fn test(&self, input: &str) -> Self {
        input.parse().unwrap()
    }
}

impl Test for u32 {
    fn test(&self, input: &str) -> Self {
        input.parse().unwrap()
    }
}

fn main() {
    let mut var = 0u32;
    let mut st = String::default();

    var = var.test("12345678");
    st = st.test("Text");
    println!("{}, {}", var, st);
}

I know this code is not perfect, and I should be using a Result return instead of unwrapping, but please set this aside as this is a quick example. The implementations for u32 and String are exactly the same, so I would like to use a default implementation for both instead of copying & pasting the code. I have tried using one, but as the returned type Self differs in both, compiler cannot determine the type size and errors.

How could I write a default implementation in this case?


Solution

  • Default implementation

    The following bounds on Self are required for the default implementation:

    1. Self: Sized because Self is returned from the function and will be placed in the caller's stack
    2. Self: FromStr because you're calling parse() on input and expecting it to produce a value of type Self
    3. <Self as FromStr>::Err: Debug because when you unwrap a potential error and the program panics Rust wants to be able to print the error message, which requires the error type to implement Debug

    Full implementation:

    use std::fmt::Debug;
    use std::str::FromStr;
    
    trait Test {
        fn test(&self, input: &str) -> Self
        where
            Self: Sized + FromStr,
            <Self as FromStr>::Err: Debug,
        {
            input.parse().unwrap()
        }
    }
    
    impl Test for String {}
    impl Test for u32 {}
    
    fn main() {
        let mut var = 0u32;
        let mut st = String::default();
    
        var = var.test("12345678");
        st = st.test("Text");
        println!("{}, {}", var, st);
    }
    

    playground


    Generic implementation

    A generic blanket implementation is also possible, where you automatically provide an implementation of Test for all types which satisfy the trait bounds:

    use std::fmt::Debug;
    use std::str::FromStr;
    
    trait Test {
        fn test(&self, input: &str) -> Self;
    }
    
    impl<T> Test for T
    where
        T: Sized + FromStr,
        <T as FromStr>::Err: Debug,
    {
        fn test(&self, input: &str) -> Self {
            input.parse().unwrap()
        }
    }
    
    fn main() {
        let mut var = 0u32;
        let mut st = String::default();
    
        var = var.test("12345678");
        st = st.test("Text");
        println!("{}, {}", var, st);
    }
    

    playground


    Macro implementation

    This implementation, similar to the default implementation, allows you to pick which types get the implementation, but it's also similar to the generic implementation, in that it doesn't require you to modify the trait method signature with any additional trait bounds:

    trait Test {
        fn test(&self, input: &str) -> Self;
    }
    
    macro_rules! impl_Test_for {
        ($t:ty) => {
            impl Test for $t {
                fn test(&self, input: &str) -> Self {
                    input.parse().unwrap()
                }
            }
        }
    }
    
    impl_Test_for!(u32);
    impl_Test_for!(String);
    
    fn main() {
        let mut var = 0u32;
        let mut st = String::default();
    
        var = var.test("12345678");
        st = st.test("Text");
        println!("{}, {}", var, st);
    }
    

    playground


    Key differences

    The key differences between the 3 approaches:

    • The default implementation makes the trait bounds inherent to the method's signature, so all types which impl Test must be sized, and have a FromStr impl with a debuggable error type.
    • The default implementation allows you to selectively pick which types get Test implementations.
    • The generic implementation doesn't add any trait bounds to the trait method's signature so a greater variety of types could potentially implement the trait.
    • The generic implementation automatically implements the trait for all types which satisfy the bounds, you cannot selectively "opt out" of the generic implementation if there are some types which you'd prefer not to implement the trait.
    • The macro implementation does not require modifying the trait method signature with additional trait bounds and allows you to selectively pick which types get the implementation.
    • The macro implementation is a macro and suffers all the downsides of being a macro: harder to read, write, maintain, increases compile times, and macros are essentially opaque to static code analyzers which makes it harder to easily type-check your code.