Skip to content

← Ground Rules: Values, Types, Control Flow step 18 of 24

Easy Primitives

Tuples and destructuring

Return the minimum, the maximum and the arithmetic mean of a slice.

pub fn min_max_mean(values: &[i32]) -> (i32, i32, f64)

[1, 2, 3] gives (1, 3, 2.0). An empty slice gives (0, 0, 0.0) — that is this function’s specified answer, not a natural one, and specifying it is the point of the first line of the body.

The starter does not compile. Its last line is wrong in two ways at once.

Tuples: the multi-value return

A tuple is an ordered, fixed-length, heterogeneous group of values written in parentheses. Its type is the tuple of its members’ types:

let pair: (i32, f64) = (7, 1.5);
let unit: ()         = ();          // the empty tuple, "unit"
let single           = (7,);        // one-element tuple needs the comma

Access is by position with a dot and a literal number:

pair.0    // 7
pair.1    // 1.5

pair.0 only accepts a literal — you cannot index a tuple with a variable, because each position has its own type and the type must be known at compile time. Asking for a field that does not exist is E0609: no field on type.

Tuples are the idiomatic way to return two or three related values without inventing a struct. Past about three fields, or as soon as the fields have names worth writing down, use a struct instead — (i32, i32, f64) tells the reader nothing about which is which, and MinMaxMean { min, max, mean } does.

Destructuring: your first pattern

You almost never write .0 and .1 in practice. You take the tuple apart:

let (min, max, mean) = min_max_mean(&values);

That left-hand side is a pattern, and this is your first pattern match — arriving before the match chapter on purpose, so that when match shows up it is a familiar idea in a new place. Patterns nest and can ignore parts:

let (min, _, mean) = triple;          // `_` discards
for (i, value) in v.iter().enumerate() { … }   // the loop you already wrote

let bindings, function parameters, for patterns, match arms, if let — they all take patterns, and it is the same grammar in every one.

The two mistakes in the starter’s last line

(min, max, sum / values.len())

Wrong types. sum is an i64 and values.len() is a usize. Rust does not implement Div<usize> for i64, so this is E0277 before it is anything else — and even if it worked, the result would be an integer where the signature promised an f64, which is E0308.

Wrong arithmetic. Integer division truncates toward zero, so the mean of [1, 2] would be 1, not 1.5. Converting to floating point after dividing would not help; you must divide in floating point.

Convert both operands to f64 and divide there.

Why the accumulator is an i64

Summing i32 into an i32 overflows, and at -O it wraps silently — you saw that in item 1.5. There is a hidden test here whose input is [i32::MAX, i32::MIN], whose true sum is -1, and whose mean is therefore -0.5. Accumulate into an i64 and it is right; accumulate into an i32 and it is not. Widening the accumulator is the cheapest correctness fix in numeric code and it costs nothing at this scale.

About comparing the mean

The mean is a floating-point number and the grader compares it with a tolerance rather than exactly, for the reasons in item 1.8. You do not need to round.