Skip to content

← Generics and Traits step 21 of 24

Medium Primitives

Extension traits: adding methods to types you do not own

Rust's orphan rule limits which trait implementations you may write. For impl Trait for Type, at least one of Trait or Type must be defined in your own crate (it must be local). You cannot implement a standard-library trait for a standard-library type:

impl std::fmt::Display for [f64] { /* ... */ } // error[E0117]: both are foreign

One way around this is a newtype: wrap the foreign type in a local struct, which makes the type local. An extension trait goes the other way. You define the trait yourself, so the trait is local, and then implement it for a type you do not own. No wrapper is needed:

pub trait StatsExt {         // local trait
    fn mean(&self) -> f64;
    fn median(&self) -> f64;
}

impl StatsExt for [f64] {    // foreign type: allowed, because the trait is local
    // ...
}

After this, any f64 slice has .mean() and .median() methods wherever StatsExt is in scope. Libraries use the same idea. For example, itertools::Itertools and futures::StreamExt are traits implemented for every type that implements Iterator or Stream respectively, and you enable their methods with use itertools::Itertools; or use futures::StreamExt;.

Your task

pub trait StatsExt { fn mean(&self) -> f64; fn median(&self) -> f64; }
impl StatsExt for [f64] { ... }

pub fn summarise(xs: Vec<f64>) -> (f64, f64)

The trait and summarise are written for you. summarise returns (mean, median). You write the impl block:

  • mean: add the values from first to last, then divide by the number of values. An empty slice returns 0.0.
  • median: sort a copy of the values in ascending order. For an odd number of values, return the middle one. For an even number, return the mean of the two middle ones, computed as (a + b) / 2.0. An empty slice returns 0.0. The input is not necessarily sorted, and must not be modified (the method only has &self).

Every input value is a finite number (the JSON test input cannot express NaN or infinity), and the test values are small enough that sums do not overflow. Example: for [10.0, 1.0, 7.0, 3.0, 2.0] the mean is 23.0 / 5.0 = 4.6 and the sorted copy is [1, 2, 3, 7, 10], so the median is 3.0. For [1.0, 2.0, 3.0, 4.0] the median is (2.0 + 3.0) / 2.0 = 2.5.

Implement it for [f64], not Vec<f64>

summarise calls xs.as_slice() first, so the receiver is a &[f64]. The trait's methods take &self, so with impl StatsExt for [f64] the self in each method has type &[f64], which matches. If you instead write impl StatsExt for Vec<f64>, the call in summarise does not compile:

error[E0599]: no method named `mean` found for reference `&[f64]` in the current scope

The slice implementation also reaches more call sites. Method lookup can dereference a Vec<f64> to [f64] and convert an array such as [f64; 8] to a slice, so v.mean(), array.mean() and v[2..5].mean() all work. An implementation for Vec<f64> only covers receivers that are or dereference to a Vec<f64> (such as &Vec<f64> or Box<Vec<f64>>); arrays and slices would not get the methods.

Sorting floats

median needs sorted data, but slice::sort requires T: Ord, and f64 does not implement Ord, because NaN is not equal to itself and is neither less than nor greater than any number. Calling .sort() on a Vec<f64> fails with E0277 (the trait bound f64: Ord is not satisfied). Two common replacements:

sorted.sort_by(f64::total_cmp);                    // total order, no panic
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());  // unwrap panics if a NaN is compared

f64::total_cmp defines an order for every f64 value, including NaN: negative NaN sorts before negative infinity, positive NaN sorts after positive infinity, and -0.0 sorts before 0.0. It never panics and needs no unwrap, so it is the safer choice even though this exercise's inputs contain no NaN.

A separate point about custom comparators: the sort_by documentation says that if the comparator is not a total order, the sort may panic, and if it returns normally the resulting order is unspecified. Do not rely on either outcome.

The trait must be in scope

An extension trait's methods can only be called where the trait is in scope. If StatsExt lived in a module stats and the caller did not import it, the call would fail like this:

error[E0599]: no method named `mean` found for reference `&[f64]` in the current scope
  = help: items from traits can only be used if the trait is in scope
help: trait `StatsExt` which provides `mean` is implemented but not in scope;
      perhaps you want to import it
  |
1 + use crate::stats::StatsExt;

The suggested use line is the fix. In this exercise the trait, the implementation and summarise are all in one file and one module, so no import is needed here.

Imports can also cause the opposite problem. If two traits in scope both provide a mean method for [f64], for example because two glob imports (use a::*; use b::*;) each bring one in, then x.mean() is ambiguous and fails with E0034 (multiple applicable items in scope). You can resolve it by calling one explicitly, such as a::A::mean(x), or by importing only the trait you need. Clippy's wildcard_imports lint warns about glob imports, but it belongs to the opt-in clippy::pedantic group, so -D warnings does not enable it.

Your submission must compile, pass the tests and pass cargo clippy -- -D warnings.