ExamplesBy LevelBy TopicLearning Paths
573 Fundamental

let-else Pattern

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "let-else Pattern" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Deeply nested `if let` expressions create the "pyramid of doom" — code that drifts rightward with each additional unwrap. Key difference from OCaml: 1. **Syntax**: Rust `let Pattern = expr else { ... }` is linear code; OCaml requires a `match` expression, potentially creating nesting.

Tutorial

The Problem

Deeply nested if let expressions create the "pyramid of doom" — code that drifts rightward with each additional unwrap. let-else (stabilized in Rust 1.65) provides early return on pattern mismatch: if the pattern does not match, the else block must diverge (return, break, continue, or panic). This enables "railway-oriented" linear code — extract what you need at the top, return on failure, use the value in the rest of the function. It is the idiomatic Rust replacement for chains of nested if let.

🎯 Learning Outcomes

  • • How let Pattern = expr else { return; } extracts a value or exits early
  • • How let-else flattens if let nesting to linear code
  • • How let-else works with Option, Result, slice patterns, and struct destructuring
  • • Why the else block must diverge: return, break, continue, panic!, or loop { ... }
  • • Where let-else is most useful: input validation, argument parsing, early-out functions
  • Code Example

    #![allow(clippy::all)]
    //! let-else Pattern
    //!
    //! Early return when pattern doesn't match.
    
    /// Basic let-else.
    pub fn get_first(v: &[i32]) -> i32 {
        let [first, ..] = v else {
            return -1;
        };
        *first
    }
    
    /// let-else with Option.
    pub fn process_option(opt: Option<i32>) -> i32 {
        let Some(value) = opt else {
            return 0;
        };
        value * 2
    }
    
    /// let-else with Result.
    pub fn process_result(res: Result<i32, &str>) -> i32 {
        let Ok(value) = res else {
            return -1;
        };
        value + 10
    }
    
    /// let-else with struct destructure.
    pub struct Config {
        pub value: Option<i32>,
    }
    
    pub fn get_config_value(c: &Config) -> i32 {
        let Some(v) = c.value else {
            return 0;
        };
        v
    }
    
    /// Multiple let-else in sequence.
    pub fn parse_pair(s: &str) -> Option<(i32, i32)> {
        let Some((a, b)) = s.split_once(',') else {
            return None;
        };
        let Ok(x) = a.trim().parse() else {
            return None;
        };
        let Ok(y) = b.trim().parse() else {
            return None;
        };
        Some((x, y))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_get_first() {
            assert_eq!(get_first(&[1, 2, 3]), 1);
            assert_eq!(get_first(&[]), -1);
        }
    
        #[test]
        fn test_process_option() {
            assert_eq!(process_option(Some(5)), 10);
            assert_eq!(process_option(None), 0);
        }
    
        #[test]
        fn test_process_result() {
            assert_eq!(process_result(Ok(5)), 15);
            assert_eq!(process_result(Err("error")), -1);
        }
    
        #[test]
        fn test_get_config() {
            let c = Config { value: Some(42) };
            assert_eq!(get_config_value(&c), 42);
        }
    
        #[test]
        fn test_parse_pair() {
            assert_eq!(parse_pair("1, 2"), Some((1, 2)));
            assert_eq!(parse_pair("invalid"), None);
        }
    }

    Key Differences

  • Syntax: Rust let Pattern = expr else { ... } is linear code; OCaml requires a match expression, potentially creating nesting.
  • Divergence requirement: Rust's else block must diverge — the compiler enforces this; OCaml's None arm can return any value (not just diverging ones).
  • Scope of binding: Rust's let-else makes the bound value available for the rest of the function (not just inside a then-block); OCaml match scopes the binding to each arm.
  • Readability impact: let-else is praised for enabling "happy path" linear code; OCaml match at the top of a function achieves similar clarity with explicit arms.
  • OCaml Approach

    OCaml's equivalent uses match with early return via Option.value or explicit pattern:

    let process_option opt =
      match opt with
      | None -> 0
      | Some value -> value * 2
    

    OCaml does not have let-else syntax — the match expression achieves the same semantics.

    Full Source

    #![allow(clippy::all)]
    //! let-else Pattern
    //!
    //! Early return when pattern doesn't match.
    
    /// Basic let-else.
    pub fn get_first(v: &[i32]) -> i32 {
        let [first, ..] = v else {
            return -1;
        };
        *first
    }
    
    /// let-else with Option.
    pub fn process_option(opt: Option<i32>) -> i32 {
        let Some(value) = opt else {
            return 0;
        };
        value * 2
    }
    
    /// let-else with Result.
    pub fn process_result(res: Result<i32, &str>) -> i32 {
        let Ok(value) = res else {
            return -1;
        };
        value + 10
    }
    
    /// let-else with struct destructure.
    pub struct Config {
        pub value: Option<i32>,
    }
    
    pub fn get_config_value(c: &Config) -> i32 {
        let Some(v) = c.value else {
            return 0;
        };
        v
    }
    
    /// Multiple let-else in sequence.
    pub fn parse_pair(s: &str) -> Option<(i32, i32)> {
        let Some((a, b)) = s.split_once(',') else {
            return None;
        };
        let Ok(x) = a.trim().parse() else {
            return None;
        };
        let Ok(y) = b.trim().parse() else {
            return None;
        };
        Some((x, y))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_get_first() {
            assert_eq!(get_first(&[1, 2, 3]), 1);
            assert_eq!(get_first(&[]), -1);
        }
    
        #[test]
        fn test_process_option() {
            assert_eq!(process_option(Some(5)), 10);
            assert_eq!(process_option(None), 0);
        }
    
        #[test]
        fn test_process_result() {
            assert_eq!(process_result(Ok(5)), 15);
            assert_eq!(process_result(Err("error")), -1);
        }
    
        #[test]
        fn test_get_config() {
            let c = Config { value: Some(42) };
            assert_eq!(get_config_value(&c), 42);
        }
    
        #[test]
        fn test_parse_pair() {
            assert_eq!(parse_pair("1, 2"), Some((1, 2)));
            assert_eq!(parse_pair("invalid"), None);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_get_first() {
            assert_eq!(get_first(&[1, 2, 3]), 1);
            assert_eq!(get_first(&[]), -1);
        }
    
        #[test]
        fn test_process_option() {
            assert_eq!(process_option(Some(5)), 10);
            assert_eq!(process_option(None), 0);
        }
    
        #[test]
        fn test_process_result() {
            assert_eq!(process_result(Ok(5)), 15);
            assert_eq!(process_result(Err("error")), -1);
        }
    
        #[test]
        fn test_get_config() {
            let c = Config { value: Some(42) };
            assert_eq!(get_config_value(&c), 42);
        }
    
        #[test]
        fn test_parse_pair() {
            assert_eq!(parse_pair("1, 2"), Some((1, 2)));
            assert_eq!(parse_pair("invalid"), None);
        }
    }

    Deep Comparison

    OCaml vs Rust: pattern let else

    See example.rs and example.ml for implementations.

    Exercises

  • Parse int: Write fn parse_positive(s: &str) -> Option<u32> using let-else to unwrap str::parse::<u32>() and return None if parsing fails or the result is zero.
  • Multi-level extraction: Write a function with three sequential let-else statements extracting from a Config struct — show that each extraction can use variables from previous ones.
  • Slice parsing: Implement fn parse_rgb(parts: &[&str]) -> Option<(u8, u8, u8)> using let [r_str, g_str, b_str] = parts else { return None; } followed by individual parse calls.
  • Open Source Repos