Desperate for generic implementation design pointers

Hi,

I am kind of lost here. I've gotten so used to a one style templatting in C++ that Rust solution seams to me as very difficult to grasp, but I must get rid of my old habits and learn new ones. In one of my previous posts I asked for a concrete advice but realized there is so much ahead of me, that I decided to post a more general best practice solution inquiry to the community.

What I have are huge code blocks, algorithms that deal with graphs. In C they were explicitly defined for 8 data types (u/i8, u/i16, u/i32, u/i64) resulting in a big code base that latter broke the library due to multiple changes being introduced for different types. Afterwords I re-implemented the library in C++ using template programming having only one template function. That worked fine and all commits applied to all data types. Now, seeing how Rust is becoming a new C/C++ standard I wish to re-implement the libraries once again. However, in order to reduce my headbanging i ask for your help.

A really small example:

pub type A<T> = Vec<T>;

pub trait Trait<T> {
    fn compute(x: T) -> A<T>;
}


impl Trait<u32> for A<u32> 
{
    fn compute(x:u32) -> A<u32>{
        let mut v : A<u32> = Vec::new();
        let mut i = x;
        while i+i < x*x {
            v.push(i);
            i= i+1;
        }
        v
    } 
}

impl Trait<i32> for A<i32> 
{
    fn compute(x:i32) -> A<i32>{
        let mut v : A<i32> = Vec::new();
        let mut i = x;
        while i+i < x*x {
            v.push(i);
            i= i+1;
        }
        v
    } 
}

impl Trait<u64> for A<u64> 
{
    fn compute(x:u64) -> A<u64>{
        let mut v : A<u64> = Vec::new();
        let mut i = x;
        while i+i < x*x {
            v.push(i);
            i= i+1;
        }
        v
    } 
}

impl Trait<i64> for A<i64> 
{
    fn compute(x:i64) -> A<i64>{
        let mut v : A<i64> = Vec::new();
        let mut i = x;
        while i+i < x*x {
            v.push(i);
            i= i+1;
        }
        v
    } 
}


fn main (){
    let z = 4i32;
    let u = 4u64;
    let a = A::compute(z);
    let b=  A::compute(u);
    println!("{:?},{:?}", a,b);
}

So here I would like to have only one function that accepts all 4 types. The reason I would like to stick to traits is because the underlining function can than be implemented for different, generic in design, objects and still have a proper interface (proper according to my personal view). How would one resolve this problem and still leave loops in their primitive forms as I am afraid that if i mess with them I will mess with speed, efficiency and readability (at least this was the reason I left them like that initially when i saw a proper OO wrapping affected speed and readability of the initial code). Any advice on how to reduce code base and preserve readability/execution speed, is much appreciated. Keep in mind I am still new to Rust and do not know much of the language features some of you may consider trivial, so any advice is more than welcomed .

Thank you !

1 Like

You can write a generic implementation. An important difference of Rust's generics to C++'s template is you have to declare every trait used in the implementation.

A generic implementation is like this:

impl<T> Trait<T> for A<T>
where T: Copy + Add<Output = T> + Mul<Output = T> + PartialOrd + From<u8>
{
    fn compute(x: T) -> A<T> {
        let mut v : A<T> = Vec::new();
        let mut i = x;
        while i+i < x*x {
            v.push(i);
            i = i + T::from(1);
        }
        v
    } 
}

Meaning of trait bounds are:

  1. Add<Output = T> and Mul<Output = T>: because I'm using + operator and * operator. Output (the operation result type) is specified because it can be a different type.
  2. PartialOrd: using < operator.
  3. Copy: if specified, we can use the same variable multiple times. It can be weakened to Clone using explicit clone calls but if your expected type is primitive number types then Copy is fine.
  4. From<u8>: this is a bit tricky. The original code uses i + 1. A numeric literal can be any of primitive types and compiler inferences the type. But this inference is not friendly with generics. Thus I explicitly converted one primitive type u8 to T by from.

Another solution is using one of the traits of num-traits crate such as PrimInt. This way simplifies the trait bound and is a handy way if you don't worry that the code won't be maximally generic.

5 Likes