Tiny Etude in Generics

Although Rust is mostly noted for it’s memory safety and thus most prominent feature is borrow checker, it has also very decent type system, which was inspired by modern functional languages like OCAML or Haskel. In this article I’ll look into very simple example, which will however show some nice features of Rust type system – especially generics.

So problem is very simple –  create a product of an vector of values (in Rust it would be a slice type, which is more general) in most generic way, so it can be used on broadest possible range of value types ( the problem is of course solve in standard library, but this is meant as a small exercise).

So let’s start with first attempt – we know for sure that our generic type needs to implement Mul trait (to multiply values) :

fn product<T>(vals: &[T])-> T
 where T: std::ops::Mul
    
 {
     assert!(vals.len()>0);
     let mut res = vals[0];
     for x in &vals[1..] {
         res = res * (*x);
     }
     
     res
 }

As often with first attempts in Rust this one even does not compile. There are two issues here:

  • Result of multiplication is associated type of Mul trait, but we are returning type T – we have to be explicit here and link T to Mul Output
  • we cannot just move or deference values out of the slice.  T has to be Copy for this code to work.

So fixed version looks like this:

fn product<T>(vals: &[T])-> T
 where T: std::ops::Mul<Output=T>+Copy
    
 {
     assert!(vals.len()>0);
     let mut res = vals[0];
     for x in &vals[1..] {
         res = res * (*x);
     }
     
     res
 }

So now it works for all Copy types, but it’s still restrictive – we can make it easily work with all Clone types (which is superset of Copy types):

fn product<'a, T>(vals: &'a[T])-> T
 where T: std::ops::Mul<Output=T> + Clone,
    
 {
     assert!(vals.len()>0);
     let mut res = vals[0].clone();
     for x in &vals[1..] {
         res = res * x.clone();
     }
     
     res
 }

However this approach is a bit inefficient, because we need to clone every value. If we look how Mul is implemented for standard numeric types, we see that right hand side could be also a reference to value (&T). This looks promising , as we will not need to clone in the loop. The only other quirk is that lifetime for &T has to be explicit here – &’a T:

fn product<'a, T>(vals: &'a[T])-> T
 where T: std::ops::Mul<&'a T,Output=T> + Clone,
    
 {
     assert!(vals.len()>0);
     let mut res = vals[0].clone();
     for x in &vals[1..] {
         res = res * x;
     }
     
     res
 }

Now we can replace loop with more elegant iterator:

fn product<'a, T>(vals: &'a[T])-> T
 where T: std::ops::Mul<&'a T,Output=T> + Clone,
    
 {
     assert!(vals.len()>0);
     vals[1..].iter().fold(vals[0].clone(), |acc,x| acc*x) 
 }

Now question is if we can get rid of Clone dependency?  Actually we do not need first value of the slice, we can start with multiplicative identity – e.g. 1.   Crate num provides this concept:

extern crate num;
use num::One;
  
fn product<'a, T>(vals: &'a[T])-> T
 where T: std::ops::Mul<&'a T,Output=T> + One,
    
 {
     assert!(vals.len()>0);
     vals.iter().fold(T::one(), |x,y| x*y)
     
 }

It looks nice and clean, but there is one practical problem –  One is not implemented for floating point types (because of quirks of binary FP calculations in real computer).

So finally we can take a look how things are properly implemented in standard library. Iterator trait provides this method:

fn product<P>(self) -> P
        where Self: Sized,
              P: Product<Self::Item>,
    {
        Product::product(self)
    }

So it’s works for all types that implements trait Product:

pub trait Product<A = Self> {
    fn product<I>(iter: I) -> Self
    where
        I: Iterator<Item = A>;
}

And Product trait is implemented for all numerical types (including floating point values) for both value and reference iterators.

So our function could use it as:

use std::iter::Product;
fn product<'a, T>(vals: &'a[T])-> T
 where T: Product<&'a T>,
    
 {
     assert!(vals.len()>0);
     vals.iter().product()
 }

 

Conclusions

Rust type system is quite powerful, which can be seen even in such simple example as is product of vector of values.

Leave a Reply

Your email address will not be published. Required fields are marked *