ExamplesBy LevelBy TopicLearning Paths
578 Fundamental

Pattern Exhaustiveness

Functional Programming

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

  • • How the Rust compiler verifies that match covers all cases
  • • How adding a new variant to an enum causes compile errors in unupdated matches
  • • How _ wildcard provides a catch-all that satisfies exhaustiveness
  • • How #[non_exhaustive] allows library enums to add variants without breaking downstream code
  • • Where exhaustiveness checking prevents real bugs: state machines, command dispatch, protocol handling
  • Code 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 missing

    Key Differences

  • Warning vs error: Rust exhaustiveness failure is a compile error; OCaml's is a warning (treated as error with -warn-error).
  • **#[non_exhaustive]**: Rust has #[non_exhaustive] for library extensibility; OCaml achieves this with private constructors or abstract module signatures.
  • Integer exhaustiveness: Both languages require _ for integer matches since integers have infinite cases; both compile without _ for finite closed enums.
  • Nested exhaustiveness: Both check exhaustiveness recursively — a nested enum variant missing coverage causes an error at the appropriate depth.
  • 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");
        }
    }
    ✓ Tests Rust test suite
    #[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

    AspectOCamlRust
    Missing caseWarning (can be ignored)Compile error
    Non-exhaustiveConvention only#[non_exhaustive] attribute
    Range matchingNot supported in matchstart..=end patterns
    Wildcard__ (same)

    Exercises

  • Add variant: Add NE, NW, SE, SW to the Dir enum and update all match expressions — observe exactly which files and lines the compiler flags.
  • Non-exhaustive library: Implement a #[non_exhaustive] pub enum ApiError and show that external code using it must include a _ arm — explain what future-proofing this provides.
  • Nested exhaustiveness: Create 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.
  • Open Source Repos