Skip to content

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

Easy Primitives

Simplify stack control flow with pattern matching

Simplify the starter's control flow while preserving its stack operations.

pub fn drain_stack(ops: Vec<String>) -> Vec<i64>

Process commands in input order, starting with an empty stack:

  • A string starting with the exact prefix "push " parses the remaining text as i64. Push the value if parsing succeeds; otherwise skip the command.
  • The exact string "clear" empties the stack.
  • Skip every other string.

Do not trim commands or their arguments. "push +007" pushes 7, but "push 7", "push 7 ", "Push 7" and " push 7" do nothing. The parser accepts decimal integers with an optional sign and leading zeros. Empty arguments, underscores, non-ASCII digits and values outside i64 fail. "clear " does not clear the stack.

After processing all commands, repeatedly pop the stack and append twice each popped value to the result. Preserve duplicates. This reverses the surviving push order: ["push 1", "push 2", "push 3"] gives [6, 4, 2]. ["push 1", "clear", "push -5"] gives [-10]. Empty input, or a final clear with no later valid push, produces an empty vector.

With the runner's current optimized execution settings, overflowing i64 multiplication wraps in 64 bits. Pushing i64::MAX then draining yields -2; pushing i64::MIN yields 0. This describes the runner's current build: checked builds instead panic on overflow. The accepted parse range is the full i64 range, not just the values whose doubles fit.

Use the construct that matches the branch

The starter produces the expected results but fails Clippy. Its one-arm matches and pop loop can be expressed more directly. A match remains useful when several patterns need different behavior; it is not inherently bad.

if let executes a branch when a pattern matches. Inside that branch, let ... else can handle failure immediately and leave a successful binding available in the surrounding block. This complete example skips malformed push arguments:

fn main() {
    let ops = ["push 2", "ignore", "push x", "push -3"];
    let mut values = Vec::new();
    for op in ops {
        if let Some(rest) = op.strip_prefix("push ") {
            let Ok(value) = rest.parse::<i64>() else {
                continue;
            };
            values.push(value);
        }
    }
    assert_eq!(values, [2, -3]);
}

Ok(value) is a refutable pattern because the parser could return Err. The else branch must diverge: it cannot continue to the statement after the binding. Here continue starts the next iteration. Return, an applicable break, panic or a non-returning call can also provide divergence.

The else branch cannot supply a fallback value. This complete program is intentionally invalid and produces E0308:

fn value_or_zero(maybe: Option<i64>) -> i64 {
    let Some(n) = maybe else { 0 };
    n
}

fn main() {
    assert_eq!(value_or_zero(None), 0);
}

If a fallback is what you want, use maybe.unwrap_or(0) or a match instead. See the let statement rules.

Repeat while a pattern matches

This complete example drains a stack without doubling:

fn main() {
    let mut stack = vec![1, 2, 3];
    let mut popped = Vec::new();
    while let Some(top) = stack.pop() {
        popped.push(top);
    }
    assert_eq!(popped, [3, 2, 1]);
    assert!(stack.is_empty());
}

Pop removes an element on every successful iteration, so this loop eventually receives None. More generally, a while-let loop ends when its pattern fails or control leaves the loop. Progress can happen in the condition or the body; a condition that does not itself mutate state is not necessarily an infinite loop. Check the whole loop when reasoning about termination.

Clippy's single_match and while_let_loop diagnostics identify relevant replacements in this starter. An equality check is sufficient for clear. Keep the required behavior rather than trying to reach a particular number of diagnostics. Passing requires compilation, correct results and Clippy with warnings treated as errors.