1

I'm trying to write a function which takes a slice of numbers and calculates the mean.

I tried using the ideas from Implementing mean function for generic types but get an error.

My code is:

extern crate num;

use num::{FromPrimitive, Zero};
use std::ops::{Add, Div};

fn main() {
    let mut numbers = [10, -21, 15, 20, 18, 14, 18];
    let err = "Slice is empty.";

    println!("Mean is {:.3}", mean(&numbers).expect(err));
}

fn mean<T>(numbers: &[T]) -> Option<f64>
where
    T: Copy + Zero + Add<T, Output = T> + Div<T, Output = T> + FromPrimitive,
{
    match numbers.len() {
        0 => None,
        _ => {
            let sum = numbers.iter().sum: ();
            let length = FromPrimitive::from_usize(numbers.len()).unwrap();
            Some(sum / length)
        }
    }
}

The error is:

error[E0658]: type ascription is experimental (see issue #23416)
  --> src/main.rs:20:23
   |
20 |             let sum = numbers.iter().sum: ();
   |                       ^^^^^^^^^^^^^^^^^^^^^^

Is there any way of writing a generic mean function without using experimental features?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
blokeley
  • 6,726
  • 9
  • 53
  • 75
  • 2
    The question you linked does not suggest writing `sum: ()`. That's not valid syntax in Rust today and I can only assume it was a typo – trent Jan 13 '19 at 12:21
  • Try to infer type of `length` and `sum` yourself. And you'll realize why compiler can't do this. Spoiler: there may be many types that implement `num::FromPrimitive`. And there may be many types that implement `std::iter::Sum<&T>`. Just like there may be many types that implement `std::ops::Div<_, Output = f64>` (_ here is because type of `length` can't be infered). – L117 Jan 13 '19 at 12:51
  • And for inference to work you must ensure that there's no ambiguities. – L117 Jan 13 '19 at 12:53
  • Heavily related: https://stackoverflow.com/q/41017140/1233251 – E_net4 Jan 13 '19 at 15:36

4 Answers4

7

You are doing 2 different operations in your generic function:

  • Summing the all values in slice : You need to tell that your elements are summable by adding Sum<T> boundary to your generic type parameter.
  • Division operation with 2 elements : Your generic type needs to be converted to f64 or any float type that you want to limit. Since you are using num crate i added ToPrimitive as boundary which tells that your generic type can be converted to a primitive type.

Here is the implementation :

fn mean<'a, T: 'a>(numbers: &'a [T]) -> Option<f64>
where
    T: ToPrimitive + Sum<&'a T>,
{
    match numbers.len() {
        0 => None,
        _ => {
            let sum = numbers.iter().sum::<T>();
            let length = f64::from_usize(numbers.len())?;

            T::to_f64(&sum).map(|sum| sum / length)
        }
    }
}

Playground

Ömer Erden
  • 7,680
  • 5
  • 36
  • 45
  • 1
    Thanks ever so much. Coming from years of Python, I'm astonished at how complicated the traits can get so quickly. I would have never worked out I need traits like Zero and ToPrimitive from a third party library to do something as simple (seemingly) as taking the mean of some numbers. – blokeley Jan 14 '19 at 16:09
  • 2
    @blokeley *as taking the mean of some numbers* — that's the "problem": you aren't taking the mean of "some numbers". You are taking the mean of any generic type that can express some set of properties. In Python, you can pass in a collections of strings to the equivalent function and get a runtime error. The equivalent Rust code enforces that whatever you do is valid when the code is compiled. You could make your *own* trait and implement it just for numbers to reduce the apparent complexity of this function. – Shepmaster Jan 15 '19 at 00:43
3

The other answers will likely help you with your real problem of writing this function generically.


The actual error you've asked about though is just a syntax mistake. You wrote this:

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

But almost certainly intended to write:

let sum = numbers.iter().sum();

The compiler has seen the : that you have accidentally included, and thinks that you are trying to use type ascription. Type ascription is syntax to use type annotations inline within an expression, instead of just in variable declarations.

What you wrote is very similar to:

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

If you were to enable type ascription in a nightly rustc build, the error would change because now the compiler will tell you that sum is a function and definitely does not have type ().

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
1

How about this:

use std::iter::Sum;

fn main() {
    let err = "Slice is empty.";

    // Test vector of integers
    let numbers = vec![10i32, -21, 15, 20, 18, 14, 18];
    println!("Mean is {:.3}", mean(numbers.into_iter()).expect(err));

    // Test vector of floating point numbers
    let numbers = vec![10f64, -21f64, 15f64, 20f64, 18f64, 14f64, 18f64];
    println!("Mean is {:.3}", mean(numbers.into_iter()).expect(err));

    // Test empty vector
    let numbers: Vec<i32> = Vec::new();    
    println!("Mean is {:.3}", mean(numbers.into_iter()).expect(err));
}

fn mean<T, I: Iterator<Item = T>>(iter: I) -> Option<f64>
where
    T: Into<f64> + Sum<T>,
{
    let mut len = 0;
    let sum = iter
        .map(|t| {
            len += 1;
            t
        })
        .sum::<T>();

    match len {
        0 => None,
        _ => Some(sum.into() / len as f64)
    }
}

Same code in the Rust Playground

It seems to have the following advantages over the answers posted so far:

  1. Much simpler generic type definition.
  2. No reliance on external num crate.
  3. No need for difficult-to-guess traits like FromPrimitive and Zero.
  4. No manual lifetimes declarations.

Or this version which has the following differences to the one above:

  1. Can take arrays rather than vectors.
  2. Does not consume the array (or vector).
  3. Needs manual lifetime declarations.
use std::iter::Sum;

fn main() {
    let err = "Slice is empty.";

    // Test aray of integers
    let numbers = [10, -21, 15, 20, 18, 14, 18];
    println!("Mean is {:.3}", mean(numbers.iter()).expect(err));

    // Test array of floating point numbers
    let numbers = [10f64, -21f64, 15f64, 20f64, 18f64, 14f64, 18f64];
    println!("Mean is {:.3}", mean(numbers.iter()).expect(err));

    // Test empty array
    let numbers: [i32; 0] = [];
    match mean(numbers.iter()) {
        Some(mean_) => println!("Mean is {:.3}", mean_),
        None => println!("Empty array"),
    }
}

fn mean<'a, T, I>(iter: I) -> Option<f64>
where
    T: Into<f64> + Sum<&'a T> + 'a,
    I: Iterator<Item = &'a T>,
{
    let mut len = 0;
    let sum = iter
        .map(|t| {
            len += 1;
            t
        })
        .sum::<T>();

    match len {
        0 => None,
        _ => Some(sum.into() / len as f64),
    }
}

Thanks to my friend Sven for code contribution.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
blokeley
  • 6,726
  • 9
  • 53
  • 75
0
  • When the compiler can't figure out the type S of fn sum<S>(self) -> S, you either need to write let foo: Bar = baz.sum(); or let foo = baz.sum::<Bar>();

  • If you are sure that T will always be some type of number primitive, you ought to collect a owned type from sum() with let sum: T = numbers.iter().cloned().sum(); and add the core::iter::Sum bound to T. Otherwise, you may want to work with references.

  • You can make your function a little more generic be returning Option<T> but if you really want to return Option<f64>, you should cast T to f64 using the ToPrimitive trait. Like this.

Caio
  • 3,178
  • 6
  • 37
  • 52
  • 1
    if i can't use integer types as input why would i make generic mean method ? – Ömer Erden Jan 13 '19 at 12:04
  • In this particular use-case, it is possible to use an array of integers (`u8`, `i32`, or `u64`) as input. The only downside would be the division truncation, i.e., `println!("{:.3}", _)` won't never display floating numbers. – Caio Jan 13 '19 at 12:23
  • 1
    Sure, but unfortunately this kills the purpose(quality) of the mean function. – Ömer Erden Jan 13 '19 at 12:25
  • You are right, @ÖmerErden. I updated the answer in a properly way. Thanks! – Caio Jan 13 '19 at 12:41
  • @Caio Thanks for the tips. I thought it best to return `Option` because the mean of an array of integers is fairly likely to be a floating point number and I didn't want to truncate the answer. – blokeley Jan 15 '19 at 09:24