Pattern Exhaustiveness
Tutorial Video
Text description (accessibility)
This video demonstrates the "Pattern Exhaustiveness" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Exhaustiveness checking is one of the most valuable compile-time guarantees in a language with algebraic data types. Key difference from OCaml: 1. **Warning vs error**: Rust exhaustiveness failure is a compile error; OCaml's is a warning (treated as error with `
Tutorial
The Problem
Exhaustiveness checking is one of the most valuable compile-time guarantees in a language with algebraic data types. When you add a new variant to an enum, the compiler immediately points to every match expression that does not handle it — no runtime surprises, no silent fallthrough to wrong behavior. This makes refactoring safe: you cannot forget to handle new cases. It is why functional programmers value algebraic data types over class hierarchies with virtual dispatch, and why Rust and OCaml are preferred for compiler writing, protocol implementations, and state machines.
🎯 Learning Outcomes
match covers all cases_ wildcard provides a catch-all that satisfies exhaustiveness#[non_exhaustive] allows library enums to add variants without breaking downstream codeCode Example
enum Dir { N, S, E, W }
fn describe(d: Dir) -> &'static str {
match d {
Dir::N => "north",
Dir::S => "south",
Dir::E => "east",
Dir::W => "west",
}
}
// Compiler errors if any case is missingKey Differences
-warn-error).#[non_exhaustive]**: Rust has #[non_exhaustive] for library extensibility; OCaml achieves this with private constructors or abstract module signatures._ for integer matches since integers have infinite cases; both compile without _ for finite closed enums.OCaml Approach
OCaml has the same exhaustiveness checking:
type dir = N | S | E | W
let describe = function
| N -> "north" | S -> "south" | E -> "east" | W -> "west"
(* Adding NE causes: Warning 8: this pattern-matching is not exhaustive *)
Both compilers warn on missing variants when the _ wildcard is absent.
Full Source
#![allow(clippy::all)]
//! # Pattern Exhaustiveness
//!
//! Rust's match expressions must cover all possible cases at compile time.
/// Direction enum for demonstration.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dir {
N,
S,
E,
W,
}
/// Describe a direction - all cases covered, no wildcard needed.
pub fn describe(d: Dir) -> &'static str {
match d {
Dir::N => "north",
Dir::S => "south",
Dir::E => "east",
Dir::W => "west",
// No _ needed: all variants covered → compile-time guarantee!
}
}
/// Check if direction is horizontal.
pub fn horizontal(d: Dir) -> bool {
match d {
Dir::E | Dir::W => true,
_ => false,
}
}
/// Alternative using matches! macro.
pub fn horizontal_matches(d: Dir) -> bool {
matches!(d, Dir::E | Dir::W)
}
/// Library-style enum with #[non_exhaustive] for future extensibility.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusCode {
Ok,
NotFound,
Unauthorized,
ServerError,
}
/// Convert status code to text - requires wildcard due to #[non_exhaustive].
pub fn status_text(c: StatusCode) -> &'static str {
match c {
StatusCode::Ok => "OK",
StatusCode::NotFound => "Not Found",
StatusCode::Unauthorized => "Unauthorized",
StatusCode::ServerError => "Internal Server Error",
_ => "Unknown", // required by #[non_exhaustive]
}
}
/// Classify an integer - exhaustive range matching.
pub fn classify(n: i32) -> &'static str {
match n {
i32::MIN..=-1 => "negative",
0 => "zero",
1..=i32::MAX => "positive",
}
}
/// Alternative classification using conditionals.
pub fn classify_if(n: i32) -> &'static str {
if n < 0 {
"negative"
} else if n == 0 {
"zero"
} else {
"positive"
}
}
/// Result-based exhaustiveness example.
pub fn handle_result<T: std::fmt::Debug, E: std::fmt::Debug>(r: Result<T, E>) -> String {
match r {
Ok(v) => format!("Success: {:?}", v),
Err(e) => format!("Error: {:?}", e),
// Both variants covered - exhaustive
}
}
/// Option-based exhaustiveness.
pub fn option_to_string<T: std::fmt::Display>(opt: Option<T>) -> String {
match opt {
Some(v) => v.to_string(),
None => "None".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_all_directions() {
assert_eq!(describe(Dir::N), "north");
assert_eq!(describe(Dir::S), "south");
assert_eq!(describe(Dir::E), "east");
assert_eq!(describe(Dir::W), "west");
}
#[test]
fn test_horizontal() {
assert!(horizontal(Dir::E));
assert!(horizontal(Dir::W));
assert!(!horizontal(Dir::N));
assert!(!horizontal(Dir::S));
}
#[test]
fn test_horizontal_approaches_equivalent() {
for d in [Dir::N, Dir::S, Dir::E, Dir::W] {
assert_eq!(horizontal(d), horizontal_matches(d));
}
}
#[test]
fn test_status_text() {
assert_eq!(status_text(StatusCode::Ok), "OK");
assert_eq!(status_text(StatusCode::NotFound), "Not Found");
assert_eq!(status_text(StatusCode::Unauthorized), "Unauthorized");
assert_eq!(
status_text(StatusCode::ServerError),
"Internal Server Error"
);
}
#[test]
fn test_classify() {
assert_eq!(classify(-100), "negative");
assert_eq!(classify(-1), "negative");
assert_eq!(classify(0), "zero");
assert_eq!(classify(1), "positive");
assert_eq!(classify(100), "positive");
}
#[test]
fn test_classify_approaches_equivalent() {
for n in [-100, -1, 0, 1, 100, i32::MIN, i32::MAX] {
assert_eq!(classify(n), classify_if(n));
}
}
#[test]
fn test_handle_result() {
let ok: Result<i32, &str> = Ok(42);
let err: Result<i32, &str> = Err("oops");
assert!(handle_result(ok).contains("42"));
assert!(handle_result(err).contains("oops"));
}
#[test]
fn test_option_to_string() {
assert_eq!(option_to_string(Some(42)), "42");
assert_eq!(option_to_string::<i32>(None), "None");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_all_directions() {
assert_eq!(describe(Dir::N), "north");
assert_eq!(describe(Dir::S), "south");
assert_eq!(describe(Dir::E), "east");
assert_eq!(describe(Dir::W), "west");
}
#[test]
fn test_horizontal() {
assert!(horizontal(Dir::E));
assert!(horizontal(Dir::W));
assert!(!horizontal(Dir::N));
assert!(!horizontal(Dir::S));
}
#[test]
fn test_horizontal_approaches_equivalent() {
for d in [Dir::N, Dir::S, Dir::E, Dir::W] {
assert_eq!(horizontal(d), horizontal_matches(d));
}
}
#[test]
fn test_status_text() {
assert_eq!(status_text(StatusCode::Ok), "OK");
assert_eq!(status_text(StatusCode::NotFound), "Not Found");
assert_eq!(status_text(StatusCode::Unauthorized), "Unauthorized");
assert_eq!(
status_text(StatusCode::ServerError),
"Internal Server Error"
);
}
#[test]
fn test_classify() {
assert_eq!(classify(-100), "negative");
assert_eq!(classify(-1), "negative");
assert_eq!(classify(0), "zero");
assert_eq!(classify(1), "positive");
assert_eq!(classify(100), "positive");
}
#[test]
fn test_classify_approaches_equivalent() {
for n in [-100, -1, 0, 1, 100, i32::MIN, i32::MAX] {
assert_eq!(classify(n), classify_if(n));
}
}
#[test]
fn test_handle_result() {
let ok: Result<i32, &str> = Ok(42);
let err: Result<i32, &str> = Err("oops");
assert!(handle_result(ok).contains("42"));
assert!(handle_result(err).contains("oops"));
}
#[test]
fn test_option_to_string() {
assert_eq!(option_to_string(Some(42)), "42");
assert_eq!(option_to_string::<i32>(None), "None");
}
}
Deep Comparison
OCaml vs Rust: Pattern Exhaustiveness
Exhaustive Match
OCaml
type dir = N | S | E | W
let describe = function
| N -> "north"
| S -> "south"
| E -> "east"
| W -> "west"
(* Compiler warns if any case is missing *)
Rust
enum Dir { N, S, E, W }
fn describe(d: Dir) -> &'static str {
match d {
Dir::N => "north",
Dir::S => "south",
Dir::E => "east",
Dir::W => "west",
}
}
// Compiler errors if any case is missing
Non-Exhaustive Enums
OCaml
(* No built-in mechanism - use wildcard by convention *)
let status_text = function
| OK -> "OK"
| NotFound -> "Not Found"
| _ -> "Unknown" (* by convention for library types *)
Rust
#[non_exhaustive]
enum StatusCode { Ok, NotFound, /* ... */ }
fn status_text(c: StatusCode) -> &'static str {
match c {
StatusCode::Ok => "OK",
StatusCode::NotFound => "Not Found",
_ => "Unknown", // Required by #[non_exhaustive]
}
}
Range Patterns
OCaml
let classify n =
if n < 0 then "negative"
else if n = 0 then "zero"
else "positive"
(* No range patterns in match *)
Rust
fn classify(n: i32) -> &'static str {
match n {
i32::MIN..=-1 => "negative",
0 => "zero",
1..=i32::MAX => "positive",
}
}
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Missing case | Warning (can be ignored) | Compile error |
| Non-exhaustive | Convention only | #[non_exhaustive] attribute |
| Range matching | Not supported in match | start..=end patterns |
| Wildcard | _ | _ (same) |
Exercises
NE, NW, SE, SW to the Dir enum and update all match expressions — observe exactly which files and lines the compiler flags.#[non_exhaustive] pub enum ApiError and show that external code using it must include a _ arm — explain what future-proofing this provides.enum Outer { A(Inner), B }; enum Inner { X, Y } and write a match on Outer with nested Inner patterns — verify all four cases are covered.