Skip to content

← Errors Are Values step 2 of 24

Easy Primitives

Matching on Option

Complete two functions using pattern matching:

pub fn classify(items: Vec<Option<i32>>) -> Vec<String>
pub fn first_pair(a: Option<i32>, b: Option<i32>) -> Option<i32>

Classify every input

Return one label per input, preserving order and duplicates:

Input Label
None "none"
Some(0) "zero"
Some(n) with n < 0 "neg:{n}", including the minus sign
Some(n) with n > 0 "pos:{n}"

For example, [Some(1), Some(-2), Some(0), None] becomes ["pos:1", "neg:-2", "zero", "none"]. Empty input returns an empty vector. Format the integer directly; no absolute-value calculation is needed, even for i32::MIN.

Keep the first value when both are present

first_pair returns the integer from a only when both inputs are Some:

a b Result
Some(2) Some(9) Some(2)
Some(2) None None
None Some(9) None
None None None
Some(0) Some(0) Some(0)

Zero and negative integers are present values. The integer inside b does not affect the result; only its presence matters. Match (a, b) as a tuple so you can check both variants together.

Cover every case

The starter's classify match covers every Some value but omits None, so it fails to compile with a non-exhaustive-pattern error. Add the missing case and implement the unfinished first_pair function.

Exhaustiveness checks patterns, including the values inside a variant, not just variant names. For example, Some(0) alone does not cover Some(1). A guard such as if n < 0 adds a runtime condition; the exhaustiveness checker does not count a guarded arm as covering all values of its pattern. Include unguarded patterns that collectively cover every possible input.

Arms are considered in order. If you put a catch-all Some(n) before Some(0), the zero arm is unreachable. After handling None, zero and negative values, a final Some(n) can handle the remaining positive values.

Explicit variant arms also help when an enum changes: adding a variant can expose matches that need updating. A wildcard _ already covers new variants, so choose it when that fallback behavior is intentional.

Choose a matching form for the job

Form Useful when
match Several cases each need a result or action
if let One matching case needs work; the other can be skipped
let ... else Later code needs a binding, and failure should leave the current path
while let Repeatedly evaluate an expression while its result matches

An ordinary let Some(n) = value; cannot handle a possible None. A let-else can, provided its else branch diverges, for example by returning:

fn describe_value(value: Option<i32>) -> String {
    let Some(n) = value else {
        return "missing".to_string();
    };
    format!("value:{n}")
}

Here n is available after the statement. With if let, the binding is available inside the matching branch. Pattern matching exposes a payload; it does not change Some and None into different types.

A loop stops when its whole pattern fails

Consider an iterator of results:

let mut values = [Ok(1), Err("bad"), Ok(3)].into_iter();
let mut seen = Vec::new();
while let Some(Ok(n)) = values.next() {
    seen.push(n);
}
assert_eq!(seen, vec![1]);
assert_eq!(values.next(), Some(Ok(3)));

The loop consumes the Err item and stops because that item does not match Some(Ok(n)). It does not reach the later Ok(3) by itself. If each error needs handling, iterate over the items and explicitly match Ok and Err.

The harness represents None as JSON null and passes integers in the i32 range. The solution must compile, return the expected labels and pair, and pass clippy -D warnings.