How can I satisfy the rust compiler that u8 input for my match arms are asserted / safe?

Viewed 306

As a Rust beginner working on one of the first problems on Exercism/Rust (https://exercism.org/tracks/rust/exercises/assembly-line)
I would like to know if it is possible to constrain integer input to a range at compile-time
to be able to have a clean set of match expression cases.

Below is my current implementation of production_rate_per_hour:

pub fn production_rate_per_hour(mut speed: u8) -> f64 {
    speed = cmp::max(speed, 10);

    let cars_per_hour: u8 = 221;

    match speed {
      0 => 0.0,
      1 ..= 4 => (speed * cars_per_hour) as f64,
      5 ..= 8 => (speed * cars_per_hour) as f64 * 0.9,
      9 | 10 => (speed * cars_per_hour) as f64 * 0.77
    }
}

I am trying to write a method that accepts a single mutable u8 argument named speed that I then constrain to the range 0..=10 as follows:

speed = cmp::max(speed, 10);

I then want to match speed on all possible cases, i.e. 0..=10. But since this is a run-time check, the compiler does not see this and tells me to also match integer value 11 and higher:

Compiling assembly-line v0.1.0 (/Users/michahell/Exercism/rust/assembly-line)
error[E0004]: non-exhaustive patterns: `11_u8..=u8::MAX` not covered
  --> src/lib.rs:12:11
   |
12 |     match speed {
   |           ^^^^^ pattern `11_u8..=u8::MAX` not covered
   |
   = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
   = note: the matched value is of type `u8`

I can of course solve this by adding the following case:

// notify
_ => println!("11 or higher")
// or crash
_ => panic!("you've crashed the assembly line!");
// or do something like this:
_ => (cmp::max(speed, 10) * cars_per_hour) as f64 * 0.77;

However, I would like to know if it is possible to constrain the input range at compile-time, and having a "clean" match expression.

Is this possible, if so, how?

1 Answers

There is currently no way to express this in the type system.

I assume you mean min instead of max. The typical approach would be:

pub fn production_rate_per_hour(mut speed: u8) -> f64 {
    speed = cmp::min(speed, 10);

    let cars_per_hour: u8 = 221;

    match speed {
      0 => 0.0,
      1 ..= 4 => (speed * cars_per_hour) as f64,
      5 ..= 8 => (speed * cars_per_hour) as f64 * 0.9,
      9 | 10 => (speed * cars_per_hour) as f64 * 0.77,
      _ => unreachable!(),
    }
}

A bug in your program (e.g., accidentally raising the cap to 11) will result in a panic. If this is performance sensitive, you can use unsafe:

_ => unsafe { std::hint::unreachable_unchecked() },

If your claim that this branch is unreachable is false, you get undefined behavior.

Note that in many cases, the compiler will be able to prove the unreachable branch is, in fact, unreachable and elide it completely. This is very common for e.g. modular arithmetic:

Example

pub fn foo(v: u64) -> u8 {
    match v % 8 {
      0 => 0,
      1 ..= 4 => 1,
      5 ..= 7 => 2,
      _ => unreachable!(),
    }
}

Note that after a slight refactoring:

pub fn production_rate_per_hour(speed: u8) -> f64 {
    let speed = speed.min(10);
    let factor = match speed {
      0 => 0.0_f64,
      1 ..= 4 => 1.0,
      5 ..= 8 => 0.9,
      9.. => 0.77,
    };

    let cars_per_hour: u8 = 221;

    factor * (speed * cars_per_hour) as f64
}

There is no case where unreachable is needed. The tradeoff is you no longer get to be very explicit in the match about what values of speed are acceptable. Whether this or a panic is better depends on your context.

Related