Skip to content

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

Medium Primitives

Match guards, and why they don't count toward exhaustiveness

Grade a list of (subject, score) pairs. The pass mark depends on the subject: maths needs 70, art needs 50, everything else needs 60. A negative score is invalid data.

pub fn grade(scores: Vec<(String, i64)>) -> Vec<String>

Each entry renders as "{subject}:{verdict}" where the verdict is pass, fail or invalid. ("maths", 70) is "maths:pass"; ("maths", 69) is "maths:fail"; ("art", 69) is "art:pass".

Why a guard and not a range pattern

You met range patterns two items ago, and they look like the answer here. They are not, and the reason is precise: the bounds of a range pattern must be constants known at compile time. The threshold in this problem is a value computed at run time from the subject name, so required.. is not something you can write.

That is exactly the gap a guard fills:

match score {
    points if points < 0 => "invalid",
    points if points >= required => "pass",
    _ => "fail",
}

A guard is an ordinary if bolted to an arm, evaluated after the pattern matches and with the pattern’s bindings in scope. If the guard is false, the arm is skipped and matching continues with the next arm — this is not an early exit.

The rule that catches everyone

Guards do not count toward exhaustiveness.

match n {
    x if x > 0 => 1,
    x if x <= 0 => 0,
}

Between them those two guards cover every i64 in existence. rustc rejects it anyway:

error[E0004]: non-exhaustive patterns: `i64::MIN..=i64::MAX` not covered

The starter ships that error. It is not a limitation to route around, it is the compiler being honest about what it can prove. A guard is arbitrary Rust — it can call functions, read variables, do arithmetic. Deciding whether two such expressions are jointly total is undecidable in general, so the exhaustiveness checker does the only sound thing available: it assumes every guard might be false. The line between “the compiler proved this” and “the compiler trusted you” runs exactly here, and it is worth knowing which side of it you are standing on.

The fix is a final unguarded arm. Some case in the test suite reaches it.

Three more facts about guards

A guard applies to the whole arm, including every alternative of an or-pattern. A(n) | B(n) if n > 0 guards both, not just B. If you meant otherwise, parenthesise or split the arm.

Guards cannot mutate the scrutinee. E0301, “cannot mutably borrow in a pattern guard” — the value being matched is borrowed for the duration of the match, and a guard that changed it could invalidate the very pattern that just matched.

If-let guards are stable. Some(x) if let Some(n) = x.checked_add(1) => ... became stable in Rust 1.95. Plenty of material online still calls this unstable; it is not.

The lint that pushes work into patterns

redundant_guards is on by default and rewrites this:

Some(x) if x == 5 => ...      // becomes  Some(5) => ...

The direction is right. Anything the pattern can express belongs in the pattern, where the exhaustiveness checker can see it; the guard is for what is left over. match_same_arms and equatable_if_let live in the same neighbourhood, both allow-by-default.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…