atomsmasher
atomsmasher

Reputation: 745

How to allow function to work with integers or floats?

I found a function to compute a mean and have been playing with it. The code snippet below runs, but if the data inside the input changes from a float to an int an error occurs. How do I get this to work with floats and integers?

use std::borrow::Borrow;

fn mean(arr: &mut [f64]) -> f64 {
    let mut i = 0.0;
    let mut mean = 0.0;
    for num in arr {
        i += 1.0;
        mean += (num.borrow() - mean) / i;
    }
    mean
}

fn main() {
    let val = mean(&mut vec![4.0, 5.0, 3.0, 2.0]);
    println!("The mean is {}", val);
}

Upvotes: 10

Views: 3913

Answers (1)

user4815162342
user4815162342

Reputation: 154876

The code in the question doesn't compile because f64 does not have a borrow() method. Also, the slice it accepts doesn't need to be mutable since we are not changing it. Here is a modified version that compiles and works:

fn mean(arr: &[f64]) -> f64 {
    let mut i = 0.0;
    let mut mean = 0.0;
    for &num in arr {
        i += 1.0;
        mean += (num - mean) / i;
    }
    mean
}

We specify &num when looping over arr, so that the type of num is f64 rather than a reference to f64. This snippet would work with both, but omitting it would break the generic version.

For the same function to accept floats and integers alike, its parameter needs to be generic. Ideally we'd like it to accept any type that can be converted into f64, including f32 or user-defined types that defin such a conversion. Something like this:

fn mean<T>(arr: &[T]) -> f64 {
    let mut i = 0.0;
    let mut mean = 0.0;
    for &num in arr {
        i += 1.0;
        mean += (num as f64 - mean) / i;
    }
    mean
}

This doesn't compile because x as f64 is not defined for x of an arbitry type. Instead, we need a trait bound on T that defines a way to convert T values to f64. This is exactly the purpose of the Into trait; every type T that implements Into<U> defines an into(self) -> U method. Specifying T: Into<f64> as the trait bound gives us the into() method that returns an f64.

We also need to request T to be Copy, to prevent reading the value from the array to "consume" the value, i.e. attempt moving it out of the array. Since primitive numbers such as integers implement Copy, this is ok for us. Working code then looks like this:

fn mean<T: Into<f64> + Copy>(arr: &[T]) -> f64 {
    let mut i = 0.0;
    let mut mean = 0.0;
    for &num in arr {
        i += 1.0;
        mean += (num.into() - mean) / i;
    }
    mean
}

fn main() {
    let val1 = mean(&vec![4.0, 5.0, 3.0, 2.0]);
    let val2 = mean(&vec![4, 5, 3, 2]);
    println!("The means are {} and {}", val1, val2);
}

Note that this will only work for types that define lossless conversion to f64. Thus it will work for u32, i32 (as in the above example) and smaller integer types, but it won't accept for example a vector of i64 or u64, which cannot be losslessly converted to f64.

Also note that this problem lends nicely to functional programming idioms such as enumerate() and fold(). Although outside the scope of this already longish answer, writing out such an implementation is an exercise hard to resist.

Upvotes: 18

Related Questions