ExamplesBy LevelBy TopicLearning Paths
562 Fundamental

Pattern Guards

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Pattern Guards" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Patterns alone cannot express all matching conditions — sometimes you need to test an arbitrary Boolean expression in addition to the structural match. Key difference from OCaml: 1. **Keyword**: Rust uses `if` in guards (`arm if cond`); OCaml uses `when` (`arm when cond`).

Tutorial

The Problem

Patterns alone cannot express all matching conditions — sometimes you need to test an arbitrary Boolean expression in addition to the structural match. Pattern guards (if condition after a match arm pattern) fill this gap: they allow any expression as an additional condition, while keeping the pattern for structural decomposition. Guards are used heavily in parsers, compilers, and game logic where the same structural form can have different meanings depending on computed values.

🎯 Learning Outcomes

  • • How match n { x if x < 0 => ... } combines a binding with a Boolean guard
  • • How guards can reference bound variables from the pattern
  • • How destructured values can be tested in guards: (x, y) if x == y
  • • Why guards interact with exhaustiveness checking: the compiler cannot verify guards cover all cases
  • • Where guards are common: number categorization, range checks, coordinate classification
  • Code Example

    #![allow(clippy::all)]
    //! Pattern Guards
    //!
    //! Additional conditions with if in match arms.
    
    /// Guard with condition.
    pub fn categorize(n: i32) -> &'static str {
        match n {
            x if x < 0 => "negative",
            x if x == 0 => "zero",
            x if x < 10 => "small positive",
            _ => "large positive",
        }
    }
    
    /// Guard with multiple conditions.
    pub fn check_range(n: i32, min: i32, max: i32) -> bool {
        match n {
            x if x >= min && x <= max => true,
            _ => false,
        }
    }
    
    /// Guard with destructuring.
    pub fn process_point(point: (i32, i32)) -> &'static str {
        match point {
            (0, 0) => "origin",
            (x, y) if x == y => "diagonal",
            (x, _) if x > 0 => "positive x",
            (_, y) if y > 0 => "positive y",
            _ => "other",
        }
    }
    
    /// Guard with Option.
    pub fn check_option(opt: Option<i32>) -> &'static str {
        match opt {
            Some(x) if x > 100 => "large",
            Some(x) if x > 0 => "positive",
            Some(0) => "zero",
            Some(_) => "negative",
            None => "none",
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_categorize() {
            assert_eq!(categorize(-5), "negative");
            assert_eq!(categorize(0), "zero");
            assert_eq!(categorize(5), "small positive");
            assert_eq!(categorize(100), "large positive");
        }
    
        #[test]
        fn test_range() {
            assert!(check_range(5, 1, 10));
            assert!(!check_range(15, 1, 10));
        }
    
        #[test]
        fn test_point() {
            assert_eq!(process_point((0, 0)), "origin");
            assert_eq!(process_point((5, 5)), "diagonal");
        }
    
        #[test]
        fn test_option() {
            assert_eq!(check_option(Some(200)), "large");
            assert_eq!(check_option(None), "none");
        }
    }

    Key Differences

  • Keyword: Rust uses if in guards (arm if cond); OCaml uses when (arm when cond).
  • Exhaustiveness: Both Rust and OCaml cannot statically verify guard coverage — compilers assume guards might fail and require a fallback arm.
  • Variable scope: In both languages, guard conditions can reference variables bound in the pattern; the guard sees the destructured values.
  • NLL interaction: Rust guards can borrow values from the matched expression — NLL ensures borrows in guards do not conflict with the arm body.
  • OCaml Approach

    OCaml uses when as its guard keyword:

    let categorize n = match n with
      | x when x < 0 -> "negative"
      | 0 -> "zero"
      | x when x < 10 -> "small positive"
      | _ -> "large positive"
    

    The semantics are identical to Rust's if guards.

    Full Source

    #![allow(clippy::all)]
    //! Pattern Guards
    //!
    //! Additional conditions with if in match arms.
    
    /// Guard with condition.
    pub fn categorize(n: i32) -> &'static str {
        match n {
            x if x < 0 => "negative",
            x if x == 0 => "zero",
            x if x < 10 => "small positive",
            _ => "large positive",
        }
    }
    
    /// Guard with multiple conditions.
    pub fn check_range(n: i32, min: i32, max: i32) -> bool {
        match n {
            x if x >= min && x <= max => true,
            _ => false,
        }
    }
    
    /// Guard with destructuring.
    pub fn process_point(point: (i32, i32)) -> &'static str {
        match point {
            (0, 0) => "origin",
            (x, y) if x == y => "diagonal",
            (x, _) if x > 0 => "positive x",
            (_, y) if y > 0 => "positive y",
            _ => "other",
        }
    }
    
    /// Guard with Option.
    pub fn check_option(opt: Option<i32>) -> &'static str {
        match opt {
            Some(x) if x > 100 => "large",
            Some(x) if x > 0 => "positive",
            Some(0) => "zero",
            Some(_) => "negative",
            None => "none",
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_categorize() {
            assert_eq!(categorize(-5), "negative");
            assert_eq!(categorize(0), "zero");
            assert_eq!(categorize(5), "small positive");
            assert_eq!(categorize(100), "large positive");
        }
    
        #[test]
        fn test_range() {
            assert!(check_range(5, 1, 10));
            assert!(!check_range(15, 1, 10));
        }
    
        #[test]
        fn test_point() {
            assert_eq!(process_point((0, 0)), "origin");
            assert_eq!(process_point((5, 5)), "diagonal");
        }
    
        #[test]
        fn test_option() {
            assert_eq!(check_option(Some(200)), "large");
            assert_eq!(check_option(None), "none");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_categorize() {
            assert_eq!(categorize(-5), "negative");
            assert_eq!(categorize(0), "zero");
            assert_eq!(categorize(5), "small positive");
            assert_eq!(categorize(100), "large positive");
        }
    
        #[test]
        fn test_range() {
            assert!(check_range(5, 1, 10));
            assert!(!check_range(15, 1, 10));
        }
    
        #[test]
        fn test_point() {
            assert_eq!(process_point((0, 0)), "origin");
            assert_eq!(process_point((5, 5)), "diagonal");
        }
    
        #[test]
        fn test_option() {
            assert_eq!(check_option(Some(200)), "large");
            assert_eq!(check_option(None), "none");
        }
    }

    Deep Comparison

    OCaml vs Rust: pattern guards

    See example.rs and example.ml for implementations.

    Exercises

  • FizzBuzz with guards: Implement FizzBuzz using pattern guards: match n { n if n % 15 == 0 => "FizzBuzz", ... } without using if/else.
  • Point classification: Write fn classify_point(x: f64, y: f64) -> &'static str using nested match with guards to distinguish origin, axes, quadrants, and far points (distance > 10).
  • Guard with enum: Create a Temperature enum with Celsius(f64) and Fahrenheit(f64) and use guards to classify as "freezing", "cold", "warm", "hot" across both variants.
  • Open Source Repos