Custom traits as trait bounds

See the question in the code comment below:

trait Number: Copy + Into<f64> {}

fn average<T>(numbers: &[T]) -> f64
where T: Copy + Into<f64> {
//where T: Number { // Why doesn't this work as an alternative to the previous line?
    let mut sum = 0.0;
    for n in numbers {
        let float: f64 = (*n).into();
        sum += float;
    }
    sum / numbers.len() as f64
    // This is a shorter alternative to the code above.
    //let sum = numbers.iter().copied().map(Into::into).sum::<f64>();
}

fn main() {
    let numbers = [200u8, 255u8, 3u8];
    println!("average = {:.1}", average(&numbers)); // 152.7
}

(Playground)

Output:

average = 152.7

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.78s
     Running `target/debug/playground`

This says that every type with the trait Number must also implement both Copy and Into<f64>, but doesn't implement Number for any types. If you want that, you also need to define a blanket implementation:

impl<T> Number for T where T: Copy + Into<f64> {}

(Playground)

1 Like

I see that this works. But is it the best way to essentially create an alias for a set of traits? Suppose I have many functions with parameters that need to implement the same set of traits, in this case Copy and Into<f64>. I'm looking for a way to give a name to that combination so I don't have to repeat it many times.

Right now it's the best way to do that in stable. If you count nightly then you can use the unstable feature trait_aliases (RFC, tracking issue)

A followup question ... How horrible is this? In order to support lots of cases where I just want to say a type has to be some kind of number, why not just define a Number trait that combines all the traits that every number type implements? That way when I'm writing each function I don't have to think about which number-related traits I'm actually using. I'm not including all the number traits here, but it demonstrates the idea.

use std::ops::*;
trait Number:
    Add + AddAssign + Copy + Div + DivAssign + Into<f64> + Mul + MulAssign + Sub + SubAssign
{
}
impl<T> Number for T where
    T: Add + AddAssign + Copy + Div + DivAssign + Into<f64> + Mul + MulAssign + Sub + SubAssign
{
}

fn sum<T: Number>(numbers: &[T]) -> f64 {
    numbers.iter().copied().map(Into::into).sum::<f64>()
}

fn average<T: Number>(numbers: &[T]) -> f64 {
    sum(numbers) / numbers.len() as f64
}

fn main() {
    let numbers = [200u8, 255u8, 3u8];
    println!("average = {:.1}", average(&numbers)); // 152.7
}

That's more or less what num-traits does.

2 Likes

That looks like a perfectly good way to do it. Adopting a more fine-grained approach can make each individual function more flexible on its own, as you can easily imagine a class of types that would implement Add and Clone, but not Div or Copy (like sets in some contexts). On the flip side, a more fine grained approach means your types will only compose in certain cases, and it imposes more of a management burden on the set as a whole. If you really just need to be able to use any kind of ordinary number, your approach will probably be the least effort.

2 Likes