Help traits and generics

I have some experience with Python and Go and now want to learn Rust from scratch. I am a self taught programmer with rather limited experience.

For the purpose of tackling some of the features of Rust I have decided to implement a Dataframe module in Rust. This is only intended for my educational purposes, not for distribution.

My main questions can be summarized

  1. how/when to use generics

  2. how/when to use traits

Here is my specific struggle:

I want to implement a struct Series which contains the name and the data of the series. Given the data could be of different types (e.g. i32, f32, String) I am using Vec<T>.

struct Series<T> {
    name: String,
    elements: Vec<T>,
}

Let's say I want to implement a methods sum and product for Series I am struggling how to implement this reasonably.

Given std::iter::Sum and std:iter:Product are not implemented for String but are implemented for i32 and f32 I found the following solution:

  • define a trait IsNumeric

  • implement functions for Series<i32> and Series<f32>

trait IsNumeric {
    fn sum(&self) -> f32;
    fn product(&self) -> f32;
}

impl IsNumeric for Series<i32> {
    fn sum(&self) -> f32 {
        let v = self.values.iter().sum::<i32>();
        return v as f32
    }

    fn product(&self) -> f32 {
        let v = self.values.iter().product::<i32>();
        return v as f32
    }
}

impl IsNumeric for Series<f32> {
    fn sum(&self) -> f32 {
        let v = self.values.iter().sum::<f32>();
        return v
  }
    fn product(&self) -> f32 {
        let v = self.values.iter().product::<f32>();
        return v
    }
}

This solution works, however, it doesn't feel right given I have to write the same functions twice. How would I use generics in this case.

Is this even the right approach? Or does it make sense to tackle this problem completley different.

I tried a somewhat different approach before using Enums for Series.Elements and then apply a match pattern. However,in the end I struggled with the same problem of having to implement a function for Elements::Int32 and Elements::Float32.

#[derive(Debug)]
enum Elements {
    Text(Vec<String>),
    Int32(Vec<i32>),
    Float32(Vec<f32>),
}

#[derive(Debug)]
struct Series {
    name: String,
    values: Elements,
}

impl Series {
    pub fn new(name: &str, values: Elements) -> Series {
        let name = name.to_string();
        Series{
            name,
            values,
        }
    }

    pub fn sum(&self) {
        match &self.values {
      Elements::Text(Vec) => println!("Text..."),
            Elements::Int32(Vec) => println!("Integer"),            
            Elements::Float32(Vec) => println!("Number"),
            // Note
            // Elements::Int32(Vec) | Elements::Float32(Vec) ==> ... 
            // throws an error when compiling
        }
    }
}

I am very grateful for any help on how to approach this problem!

Thanks
Fred

This is expected. Generics in Rust aren't that flexible/dynamic. There are no common traits for numeric types.

In this case you could avoid the repetition by implementing sum and product for IntoIterator types where the Iter implements Sum and Product traits.

But such abstract implementations become really messy really quickly. In generic context you can only do things explicitly required by the bounds, so you'll end up defining every single detail, including defining that addition exists and that two numbers added together produce a number. Such code tends to produce hard to understand error messages.

You can always use a macro to avoid repetition. A little copying with a macro gives much easier to understand code than extra layers of generics abstraction.

Thanks for your swift feedback. Much appreciated!

Three comments:

  • I will look into IntoIterator types. Not sure what that is.
  • do you mind elaborating a bit on how macros can avoid repetition in this case?
  • when you say that "generics in Rust are not that flexible/dynamic", do you mean that Rust is not the right language for my problem?

Thanks again for your help!

Apologies if I've misunderstood the question, but you basically want to define sum for any Series<T> where T has a std::iter::Sum defined (like i32 and f32)? You can do that like this:

impl<T> Series<T>
where
    T: for<'a> std::iter::Sum<&'a T>,
{
    fn sum(&self) -> T {
        self.elements.iter().sum()
    }
}

Here's a playground that implements this. You can do Product in a similar fashion.

1 Like

Thanks for your solution.

Yes, I can see how this removes code duplication.

I will have to chew on this one a bit. Is it possible to implement a sum method for when T is Series<&str> to avoid missuse, e.g. a "not implemented" message? Can this be integrated in the above code?

Does this even make sense?

Thanks!

Calling sum on a Series<&str> would be a compile time error. If you need it to compile and be a runtime error, you'd probably be better off using the enum that explicitly enumerates the types your Series accepts.

.sum() can be called on things that implement Sum trait, and the Sum trait wants to be run on an Iterator. In case you wanted to abstract over type of collection you use, you'd use IntoIterator trait, so that you can covert anything -> IntoIterator -> Iterator -> Sum. If you're only going to use it with Vec<T>, then a bound on std::slice::Iter<T>: Sum<T> should suffice to let you call .iter().sum().

Thanks to both of your for your helpful comments @asymmetrikon and @kornel