Extending the exhaustive checks feature might help writing correct programs. Exhaustive checking in the match statement is currently limited to input values only. Output values might also benefit from exhaustive checks. Next to the exhaustive checks, a left- or right-uniqueness check might increase program correctness. As a specific suggestion: Aside of the term match there should also exist an injective_match, a surjective_match and a bijective_match.
Please read this as an idea. It is most likely not sound.
Minimal mathematical foundation
When it comes to exhaustiveness guarantees, there are four properties:
- right-uniqueness: Every input variable of a match has a single, unique output variable.
- left-uniqueness: Every output variable has a single, unique input variable.
- right-totality: Every possible output value is covered.
- left-totality: Every possible input value is covered.
Currently, only the left-totality exhaustive check is implemented. It is possible to switch off the check by using _ => {}.
Function table
Combining these function properties, there are four common function types. As follows, these function types are named match categories, because function is a term already used in Rust. There are four binary variables right-unique, left-total, right-total and left-unique. We can ignore the left-totality, because default matching is a feature we wish for. Which narrows it down to three binary variables. With that, only the right-unique cases are relevant, which leads to four cases:
| right-unique | left-total | right-total | left-unique | match category |
|---|---|---|---|---|
| Y | * | N | N | normal match case, the left-totality is already a compile-time guarantee. It can be switched off, by setting the Scrutinee to _, which is the default matching _ => {}. |
| Y | * | Y | N | named surjective_match or match_onto. |
| Y | * | N | Y | named injective_match. |
| Y | * | Y | Y | named bijective_matching |
| * | * | * | * | All other cases are not of relevance for Rusts matching concept. |
Example usage
The following example of a bijective_match indicates the use case. Additional exhaustiveness checks and what problems might occur, while implementing:
struct SymbolicNumbers { One, Two, Three }
// this matching is right-total
impl TryFrom<u32> for SymbolicNumbers {
type Error = ();
fn try_from(input: u32) -> Result<Self, Self::Error> {
bijective_match input { // <- the special thing here is the `bijective_match`
1 => Ok(SymbolicNumbers::One),
2 => Ok(SymbolicNumbers::Two),
3 => Ok(SymbolicNumbers::Three),
_ => { Err(()) }
}
}
}
// Same code but a case is commented out.
// So this matching is not right-total. It would cause a compile-time error.
impl TryFrom<u32> for SymbolicNumbers {
type Error = ();
fn try_from(input: u32) -> Result<Self, Self::Error> {
bijective_match input {
1 => Ok(SymbolicNumbers::One),
2 => Ok(SymbolicNumbers::Two),
// this errors in something like:
// error[EXXXX]: non-exhaustive right-totality patterns: `SymbolicNumbers::Three` not covered
// because following line is missing:
// 3 => Ok(SymbolicNumbers::Three),
_ => Err(()),
}
}
}
Possible problems
- What about nested
enums or longtuples? - What about guarded arms (containing
ifstatements)? - What about integer matching? The current
matchimplementation allows specifyingRangeand Integers at the same time forPatterns. - To check the soundness of the result types (e.g. right-totality), one would have to traverse into functions and branches within the match-arm expressions. For the current
matchimplementation (Pattern), there is not such a code traversal, as values have to be defined directly in theMatchArm. - In the example above (
3 => Ok(SymbolicNumbers::Three)): instead of returning theOk(SymbolicNumbers::Three)directly, a function call might returnOk(SymbolicNumbers::Three)instead. For instance, the body of the arm could be3 => ok_symbolic_numbers_three(). One could traverse into the return value, but this would open reachability problems. I would suggest to ignore the reachability issues:fn ok_symbolic_numbers_three() -> Option<SymbolicNumbers> { if false { // 1. the compiler could traverse into all functions return instances. // 2. this will never be reached at runtime. return Ok(SymbolicNumbers::Three); } else { // ignore this todo!(); } }
Request for comments
I did not find a similar proposal or problem description. Please help me with a link, if I can not properly use the search engine of my choice. As I like the idea, I would be really happy if this turns into a discussion! ![]()