Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 14 of 26

Medium Primitives

Refutability, and every place a pattern can appear

Compute the centroid, or coordinate-wise arithmetic mean, of a list of 2-D points.

pub fn centroid(points: Vec<(f64, f64)>) -> (f64, f64)

Return (0.0, 0.0) for an empty list. Otherwise, sum each coordinate in input order, multiply each sum by 1.0 / points.len() as f64, and round each result with the supplied round6 helper. For example, the points (1.0, 1.0), (2.0, 2.0) and (4.0, 4.0) give (2.333333, 2.333333). Assume inputs and intermediate arithmetic remain finite, including the multiplication by one million in round6.

Practice destructuring in three places: a helper's parameter list, a for binding, and a let binding for the helper's result. The starter already uses all three; repair its handling of the empty input.

Patterns outside match

A pattern describes how to recognize or unpack a value. Tuple patterns can name the parts of a parameter, a loop item or a return value:

fn scale((x, y): (f64, f64), factor: f64) -> (f64, f64) {
    (x * factor, y * factor)
}

fn main() {
    let points = [(2.0, 4.0), (6.0, 8.0)];
    let mut total = (0.0, 0.0);
    for &(x, y) in &points {
        total.0 += x;
        total.1 += y;
    }
    let (x, y) = scale(total, 0.5);
    assert_eq!((x, y), (4.0, 6.0));
}

Here for &(x, y) copies the two f64 values from each borrowed tuple. The starter's for (x, y) in &points instead binds references to them. Both work: f64 supports adding a borrowed f64 with +=.

Can this pattern fail to match?

An irrefutable pattern matches every value of the relevant type. (x, y) can unpack any pair. A refutable pattern can fail: Some(value) matches an Option containing a value, but not None.

The type matters. [a, b] is irrefutable for a two-element array, but refutable for a slice whose length can vary. A new variable binding accepts any value; a name that resolves to a constant or enum variant can instead impose a matching condition.

Function and closure parameters, for bindings, and ordinary let statements require irrefutable patterns. In the starter, points.first() returns Option<&(f64, f64)>, so this statement fails with E0005:

let Some(&(first_x, first_y)) = points.first();

The input might be empty. Handle that case explicitly before unpacking a point, or return early for an empty list and sum all points from zero. A let ... else is another option: its else branch must leave the surrounding flow, for example with return.

Refutable patterns also work in if let, while let and match arms. These constructs provide control flow for a failed match. This example uses a pattern in a closure parameter too:

fn first_x_or_zero(points: &[(f64, f64)]) -> f64 {
    let Some(&(x, _)) = points.first() else {
        return 0.0;
    };
    x
}

fn main() {
    assert_eq!(first_x_or_zero(&[]), 0.0);
    assert_eq!(first_x_or_zero(&[(2.0, 3.0)]), 2.0);
    let product = |(x, y): (i32, i32)| x * y;
    let mut stack = vec![product((2, 3)), 4];
    let mut sum = 0;
    while let Some(value) = stack.pop() {
        sum += value;
    }
    if let Ok(value) = "10".parse::<i32>() {
        assert_eq!(sum, value);
    }
}

Combining alternatives

An or-pattern combines alternatives with |. In a let binding or a function or closure parameter, wrap a top-level or-pattern in parentheses. This is a syntax requirement, separate from whether the combined pattern covers every possible value.

enum Reading {
    Measured(i32),
    Estimated(i32),
}

fn value((Reading::Measured(n) | Reading::Estimated(n)): Reading) -> i32 {
    n
}

fn main() {
    let reading = Reading::Measured(7);
    let (Reading::Measured(n) | Reading::Estimated(n)) = reading;
    assert_eq!(n, 7);
    assert_eq!(value(Reading::Estimated(9)), 9);
}

Each alternative alone is refutable, but together they cover both variants. They bind the same name with the same type and binding mode. Adding a third variant would make this combined pattern refutable until it was handled too.

Rounding for this exercise

round6(value) computes (value * 1e6).round() / 1e6. Rust's round rounds halfway cases away from zero. This is a task-specific output rule; it does not remove all floating-point error or make summation independent of input order. In other programs, choose precision and error tolerances to suit the calculation instead of automatically rounding at every boundary.

Submissions must compile, pass the value tests, and pass Clippy with warnings treated as errors. The value tests check results; review your own code for the three requested uses of destructuring.