Take n from vector

Looking forward to the day I can solve something simple in Rust on my own in less than a half hour without help. :wink: I want to take a vector of Student instances and find the top three students in terms of their average score. I'm not able to determine from the error message what type it wants. Once that is solved, it will panic and I don't understand that either.

#[derive(Debug)]
struct Student {
    name: String,
    scores: Vec<i8>,
}

impl Student {
    fn new(name: &str, scores: &[i8]) -> Self {
        Self {
            name: name.to_string(),
            scores: scores.to_vec(),
        }
    }
}

fn average(numbers: &[i8]) -> f32 {
    numbers.iter().sum::<i8>() as f32 / numbers.len() as f32
}

fn main() {
    let mut students: Vec<Student> = vec![
        Student::new("Alice", &[90, 75, 80]),
        Student::new("Betty", &[85, 95, 80]),
        Student::new("Claire", &[70, 80, 75]),
        Student::new("Dina", &[95, 100, 90]),
        Student::new("Elaine", &[75, 90, 10]),
    ];
    students.sort_by(|a, b| {
        let a_avg = average(&a.scores);
        let b_avg = average(&b.scores);
        a_avg.partial_cmp(&b_avg).unwrap()
    });
    println!("{:?}", students);
    let top_students: &[Student] = students.iter().take(3).collect();
    println!("{:?}", top_students);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: a value of type `&[Student]` cannot be built from an iterator over elements of type `&Student`
  --> src/main.rs:34:60
   |
34 |     let top_students: &[Student] = students.iter().take(3).collect();
   |                                                            ^^^^^^^ value of type `&[Student]` cannot be built from `std::iter::Iterator<Item=&Student>`
   |
   = help: the trait `FromIterator<&Student>` is not implemented for `&[Student]`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

You can only collect into owned types, but &[Student] is a borrowed type. Change the type to Vec<Student>.

4 Likes

There's a few reasonable choices for that line. Taking @alice's suggestion leads to this (with #[derive(Clone)] on Student:

let top_students: Vec<Student> = students.iter().take(3).cloned().collect();

Without the clone, you can get a Vec<&Student>:

let top_students: Vec<&Student> = students.iter().take(3).collect();

But my suggestion in this case is to use indexing instead of an iterator. The results are already contiguous in memory, so you don't need another allocation:

let top_students: &[Student] = &students[0..3];

Your panic comes from here:

fn average(numbers: &[i8]) -> f32 {
    numbers.iter().sum::<i8>() as f32 / numbers.len() as f32
}

i8 can only hold values between -128 and 127, so it isn't big enough to hold the sum of 3 grades. You need to cast the grades to a larger type before trying to sum them:

numbers.iter().map(|&x| x as f32).sum::<f32>() / numbers.len() as f32

(Playground)

6 Likes

Mapping over the i8 values to convert them to f32 seems like too much work and not something that would have occurred to me. I wonder if in terms of code understandability, the following would be preferred in the average function. I guess it's a matter of personal preference. Is one approach considered more idiomatic than the other?

    let mut sum: f32 = 0.0;
    for n in numbers {
        sum += *n as f32;
    }
    sum / numbers.len() as f32

It's mostly a matter of personal preference. Restricting average to calculating with i8s feels a little off to me. If this was part of a medium-large project, I'd consider writing it like this (or with an equivalent loop):

fn average(numbers: &[impl Copy+Into<f32>]) -> f32 {
    let total = numbers.iter().copied().map(|x| x.into()).sum::<f32>();
    total / numbers.len() as f32
}

Or even:

fn average(numbers: impl ExactSizeIterator<Item=impl Into<f32>>) -> f32 {
    let len = numbers.len() as f32;
    let total = numbers.map(|x| x.into()).sum::<f32>();
    total / len
}
1 Like

You can further simplify that by removing the closure and specifying Into::into directly.

2 Likes

I realy like that you can do things like that in Rust too, it works well with type inference. I was using similar codes in Python, e.g map(str.lower, ["Aa", "BB"]) instead of map(lambda i: i.lower(), ["Aa", "BB"])

3 Likes

Point-free style! Lack of currying makes it less effective in Rust than in (say) Haskell but it's still very pleasing.

1 Like

Can you show me what that would look like?

numbers.map(|x| x.into())

can also be written as:

numbers.map(Into::into)
1 Like