Generic function over integer slice

This seems to be a common question, but I've yet to find a good solution. In general I'd like to find an approach for writing a generic function that works for all signed and unsigned integer types. For example, I'd like to write a function that returns the average of a slice of such numbers. Here is my failed attempt.

extern crate num;
use std::iter::Sum;

fn average<T>(numbers: &[T]) -> f32
where
    T: num::PrimInt + Sum
{
    let sum: T = numbers.iter().sum() as f32;
    sum / numbers.len() as f32
}

fn main() {
    let scores = vec![70, 90, 100];
    println!("average = {:.1}", average(&scores)); // should output 86.7
}
1 Like

I got this to work with some help from Why is i32::from(f) not allowed but 'f as i32' is? - #3 by jdahlstrom .

extern crate num;
use std::iter::Sum;

pub trait ToF32 {
    fn to_f32(v: Self) -> f32;
}

impl ToF32 for i32 {
    fn to_f32(v: Self) -> f32 {
        return v as f32;
    }
}

fn average<'a, T>(numbers: &'a[T]) -> f32
where
    T: num::PrimInt + Sum<&'a T> + ToF32,
{
    let sum : T = numbers.iter().sum();
    ToF32::to_f32(sum) / numbers.len() as f32
}

fn main() {
    let scores = vec![70, 90, 100];
    println!("average = {:.1}", average(&scores)); // should output 86.7
}

Playground Link

1 Like

You can do this:

fn average<T>(numbers: &[T]) -> f32
where
    T: num::PrimInt + Clone + Sum,
{
    let sum: f32 = numbers.iter().cloned().sum::<T>().to_f32().unwrap();
    sum / numbers.len() as f32
}

The to_f32 method is available due to the PrimInt bound.

3 Likes

Whoops, I did not know that PrimInt brings to_f32. With that the custom Trait is not needed in my solution and it becomes

fn average<'a, T>(numbers: &'a[T]) -> f32
where
    T: num::PrimInt + Sum<&'a T>,
{
    let sum : T = numbers.iter().sum();
    sum.to_f32().unwrap() / numbers.len() as f32
}
1 Like

The compiler errors provide a reasonable guide for fixing this code. If we ignore everything except the first error each time, the process looks like this:

error[E0308]: mismatched types
 --> src/main.rs:8:18
  |
4 | fn average<T>(numbers: &[T]) -> f32
  |            - this type parameter
...
8 |     let sum: T = numbers.iter().sum() as f32;
  |              -   ^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected type parameter `T`, found `f32`
  |              |
  |              expected due to this
  |
  = note: expected type parameter `T`
                       found type `f32`

You're trying to store an f32 into a variable of type T. Make the variable f32 instead:

let sum: f32 = numbers.iter().sum() as f32;

error[E0282]: type annotations needed
 --> src/main.rs:8:35
  |
8 |     let sum: f32 = numbers.iter().sum() as f32;
  |                                   ^^^ cannot infer type for type parameter `S` declared on the associated function `sum`
  |
  = note: type must be known at this point
help: consider specifying the type argument in the method call
  |
8 |     let sum: f32 = numbers.iter().sum::<S>() as f32;
  |                                      ^^^^^

Ok; we're summing T's, so let's specify that the way the compiler hint suggests:

let sum: f32 = numbers.iter().sum::<T>() as f32;

error[E0277]: the trait bound `T: Sum<&T>` is not satisfied
 --> src/main.rs:8:35
  |
8 |     let sum: f32 = numbers.iter().sum::<T>() as f32;
  |                                   ^^^ the trait `Sum<&T>` is not implemented for `T`
  |
help: consider further restricting this bound
  |
6 |     T: num::PrimInt + Sum + Sum<&T>,
  |                           ^^^^^^^^^

The iterator is providing &Ts instead of Ts. Primitive integers are Copy, so we can use the copied method to get past the reference:

let sum: f32 = numbers.iter().copied().sum::<T>() as f32;

error[E0605]: non-primitive cast: `T` as `f32`
 --> src/main.rs:8:20
  |
8 |     let sum: f32 = numbers.iter().copied().sum::<T>() as f32;
  |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object

We can't use an as cast to convert between the generic type T and the concrete one f32, so we need another way to do this conversion. Fortunately, there's PrimInt::to_f32:

let sum: f32 = numbers.iter().copied().sum::<T>().to_f32();

error[E0308]: mismatched types
 --> src/main.rs:8:20
  |
8 |     let sum: f32 = numbers.iter().copied().sum::<T>().to_f32();
  |              ---   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `f32`, found enum `Option`
  |              |
  |              expected due to this
  |
  = note: expected type `f32`
             found enum `Option<f32>`

It apparently returns an Option. In the absence of anything sensible to do with the conversion failure, we can just unwrap it:

let sum: f32 = numbers.iter().copied().sum::<T>().to_f32().unwrap();

average = 86.7

Looks like we're done at this point. The process isn't particularly hard, but you do sometimes need to go several rounds with the compiler before reaching code that works.

(Playground)

12 Likes

I see that this works, but would a Rustacean view this as a good solution? Would you use this approach or would you prefer to just implement different functions for each integer type you need to support? A line like let sum: f32 = numbers.iter().cloned().sum::<T>().to_f32().unwrap(); seems pretty frightening compared to what people are used to in other languages.

I wonder if there is also a solution that just requires the type in the slice to implement traits like std::ops::Add and std::ops::Div.

Personally I would only define the method with generics if I actually need to call it with three or more different types of integers. With one or two, just hard-code it. :woman_shrugging:

A less scary method might be to convert them to floats before summing:

fn average<T>(numbers: &[T]) -> f32
where
    T: num::PrimInt,
{
    let sum: f32 = numbers.iter().map(|n| n.to_f32().unwrap()).sum();
    sum / numbers.len() as f32
}

or to use a loop:

fn average<T>(numbers: &[T]) -> f32
where
    T: num::PrimInt,
{
    let mut sum = 0.0;
    
    for num in numbers {
        sum += num.to_f32().unwrap();
    }
    
    sum / numbers.len() as f32
}
4 Likes

That is much less scary! Thanks!

How could I have discovered that the PrimInt trait supports the to_f32 method?
I don't see it at num::traits::PrimInt - Rust.

BTW: After reading alice's solution to a different topic, I saw that my proposal does not require an explicit lifetime in the signature, either:

extern crate num;
use std::iter::Sum;

fn average<T>(numbers: &[T]) -> f32
where
    T: num::PrimInt,
    T: for<'a> Sum<&'a T>
{
    let sum : T = numbers.iter().sum();
    sum.to_f32().unwrap() / numbers.len() as f32
}

fn main() {
    let scores = vec![70, 90, 100];
    println!("average = {:.1}", average(&scores)); // outputs 86.7 as expected
}

(Playground)

2 Likes

It’s in ToPrimitive which is one of the supertraits of PrimInt.

1 Like

I admit that it is not easy to find the to_f32 method on the docs. You have to click "show declaration" at the top, which reveals its super-traits. Then you find that it has NumCast as a super-trait, but that trait doesn't directly expose a suitable method (NumCast is for the opposite conversion). But then by clicking "show declaration" again, you see that ToPrimitive is a super-trait of NumCast, and the method is on ToPrimitive.

3 Likes

Taking this one step farther, I wonder if I can use the num crate to write an average function that also works for float values by using the Num trait instead of the PrimInt trait. Here is my start on it:

extern crate num;

fn average<T: num::Num>(numbers: &[T]) -> f32 {
    let mut sum = T::zero();
    for n in numbers {
        sum = sum + *n;
    }
    (sum / numbers.len()).to_f32() // errors here
}

fn main() {
    let scores = vec![70, 90, 85, 100];

    // Print average of all scores.
    println!("average = {:.1}", average(&scores)); // 86.2

    // Print average of all scores except the first.
    println!("average = {:.1}", average(&scores[1..])); // 91.7
}

Well it fails because numbers.len() might not return the type T as it always returns usize. You need to make the types match!

1 Like

Yeah, but I don't know how to convert a usize to num::Num which implements the Div trait. If I could do that, the my next problem would be finding a way to call the to_f32 method on a Num. It doesn't look like there's a way to do that, so I don't know how to provide the result type of f32.

Alternatively I could try convert sum to an f32 so I could do the division on built-in types, but I don't know how to do that either.

If you specify that T satisfies ToPrimitive instead of (or in addition to) Num, you should be good because then you can be sure that you can call to_f32 on an instance of T.

2 Likes

Making progress! Here's a working implementation.
I think I have just one more question on this.
Num implements the Add trait, so the line sum = sum + *n; works.
But Num does not implement the AddAssign trait, so it seems I cannot use sum += *n;.
However, if I say that T must implement AddAssign then I can use that.
How is it that I can just say I want that and Num magically gains that ability?

extern crate num;
use core::ops::AddAssign;
use num::{Num, ToPrimitive};

fn average<T: AddAssign + Copy + Num + ToPrimitive>(numbers: &[T]) -> f32 { 
    let mut sum = T::zero();
    for n in numbers {
        //sum = sum + *n; // only requires Add which Num implements
        sum += *n; // requires AddAssign
    }
    let numerator = sum.to_f32().unwrap();
    numerator / numbers.len() as f32
}

fn main() {
    //let scores = vec![70, 90, 85, 100];
    let scores = vec![70.1, 90.2, 85.3, 99.4];
    println!("average = {:.1}", average(&scores)); // 86.2
}

Is specifying the trait bounds in the angle brackets discouraged when there is more than one?
Is using a where clause considered more idiomatic?

Num doesn't gain the ability; you've told the compiler to reject any attempts to call average unless it can prove the given T implements all of the traits you've listed. If you had some other function like this:

fn f<T:Num + ToPrimitive + Copy>(...

then it would be unable to call average: its T doesn't necessarily implement AddAssign, so the compiler won't allow it.

4 Likes

Thanks so much for explaining that! It's a piece of the puzzle that I hadn't yet grasped.

For things that need floating-point to be reasonable, I often only care about the two primitives, so a middle ground would be to define those two functions with a macro. Perhaps something like this:

macro_rules! define_average {
    ($name:ident $t:ty) => {
        fn $name(iter: impl ExactSizeIterator<Item = $t>) -> Option<$t> {
            let len = iter.len();
            if len == 0 { None }
            else { Some(iter.sum::<$t>() / (len as $t)) }
        }
    }
}

define_average!(average_f32 f32);
define_average!(average_f64 f64);

fn main() {
    assert_eq!(average_f32(vec![1.0, 2.0, 3.0].into_iter()), Some(2.0));
    assert_eq!(average_f64(vec![1.0, 2.0, 3.0].into_iter()), Some(2.0));
}
1 Like