How to avoid "_" case in matches on modular numbers

Viewed 640

This smells bad:

let millis = time % 1000;
match millis {
    0..=199 => do_something(),
    200..=599 => do_something_else(),
    600..=999 => do_something_altogether_different(),
    _ => panic!("There are only 1,000 ms in a second."),
}

The "_" case will never be called, as millis is time % 1000.

How can I rewrite this so that I only have three branches?

Ideally, I'd also like to remove the duplication that is 199/200, 599/600 and 999/1000/0.

If the best solution is not a match I'm happy to use some other control structure.

4 Answers

Rust uses the type for matching, so it doesn't know the bounds as per your logic. But you do.

For being explicit you can use unreachable!:

Indicates unreachable code.

This is useful any time that the compiler can’t determine that some code is unreachable. For example:

Match arms with guard conditions. Loops that dynamically terminate. Iterators that dynamically terminate. If the determination that the code is unreachable proves incorrect, the program immediately terminates with a panic!.

let millis = time % 1000;
match millis {
    0..=199 => do_something(),
    200..=599 => do_something_else(),
    600..=999 => do_something_altogether_different(),
    _ => unreachable!("There are only 1,000 ms in a second."),
}

Otherwise you can consider that the last branch is the default one:

let millis = time % 1000;
match millis {
    0..=199 => do_something(),
    200..=599 => do_something_else(),
    _ => do_something_altogether_different(),
}

I would use a simple if because you have one condition per case and only a small number of cases:

if millis < 200 {
    do_something()
} else if millis < 600 {
    do_something_else()
} else {
    do_something_altogether_different()
}

If you want to use match you could also use if-guards in it:

match millis {
    x if x < 200 => do_something(),
    x if x < 600 => do_something_else(),
    _ => do_something_altogether_different(),
}

Both remove the duplication of 199/200...

You can modify one of the arms so that the compiler understands it to include the case that cannot happen, by making one of the ranges unbounded. This could be considered the match version of the if chain option:

fn foo(time: u32) {
    let millis = time % 1000;
    match millis {
        0..=199 => todo!(),
        200..=599 => todo!(),
        600.. => todo!(),
    }
}

However, I would choose this option only in code that's as obviously-correct as this example; in more complex cases I'd prefer the version with an _ => unreachable!() since that will catch any bugs resulting in actually out-of-range values. (In fact, there's such a possible hidden situation in code very similar to this! If the input is a signed integer, time % 1000 might be negative, which is out of range. Though that'd make the above not compile for not covering those patterns.)

This is just an improvement on Kevin Reid's answer, which removes the duplication by reversing the order:

let millis: u64 = time % 1000;
match millis {
    600.. => todo!(),
    200.. => todo!(),
    0.. => todo!(),
}

We could also leave out one more number, but I'm undecided as to whether this is an improvement.

let millis: u64 = time % 1000;
match millis {
    600.. => todo!(),
    200.. => todo!(),
    _ => todo!(),
}
Related