ExamplesBy LevelBy TopicLearning Paths
575 Fundamental

Let Chains (&&)

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Let Chains (&&)" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Multiple sequential pattern checks create nesting: `if let Some(x) = a { if let Ok(y) = b { if condition { ... Key difference from OCaml: 1. **Stabilization**: Rust let chains were stabilized in 1.88 — a relatively recent addition; OCaml's `let*` notation for monadic chaining has been available since 4.08.

Tutorial

The Problem

Multiple sequential pattern checks create nesting: if let Some(x) = a { if let Ok(y) = b { if condition { ... } } }. This pyramid of doom is hard to read and hard to maintain. Let chains (stabilized in Rust 1.88) allow combining multiple let bindings and Boolean conditions in a single flat if expression using &&. This is the Rust equivalent of Haskell's do notation or OCaml's match nesting, enabling linear "happy path" code for multi-step extraction.

🎯 Learning Outcomes

  • • How if let P = x && let Q = y && cond { } chains multiple pattern checks
  • • How let chains short-circuit: if a pattern fails, later patterns are not evaluated
  • • How to mix let pattern bindings with Boolean conditions in one chain
  • • How let chains replace nested if let without requiring let-else
  • • Where let chains improve: argument validation, multi-field config extraction, parser steps
  • Code Example

    fn process(s: &str) -> Option<i32> {
        if let Ok(n) = s.parse::<i32>()
            && n > 0
            && n % 2 == 0
        {
            Some(n * 2)
        } else {
            None
        }
    }

    Key Differences

  • Stabilization: Rust let chains were stabilized in 1.88 — a relatively recent addition; OCaml's let* notation for monadic chaining has been available since 4.08.
  • Mix of patterns and conditions: Rust lets you freely mix let P = x and cond in the same chain; OCaml's let* is purely monadic, requiring conditions to be lifted into Option.
  • Short-circuit: Both Rust let chains and OCaml Option.bind short-circuit on the first failure.
  • Scope: Rust let-chain bindings are visible to subsequent parts of the same chain and the body; OCaml let* bindings are visible to the continuation.
  • OCaml Approach

    OCaml achieves the same with Option.bind chains or nested match:

    let process s =
      let open Option in
      let* n = int_of_string_opt s in
      let* () = if n > 0 && n mod 2 = 0 then Some () else None in
      Some (n * 2)
    

    OCaml 4.08+ let* (monadic bind) provides a similar linear chaining style.

    Full Source

    #![allow(clippy::all)]
    //! # Let Chains (&&)
    //!
    //! Chain multiple pattern checks with `&&` — combine pattern matching
    //! and boolean conditions without nesting.
    //!
    //! Requires Rust 1.88+ for stable let chains.
    
    /// Process a string, returning doubled value if it's a positive even number.
    ///
    /// Uses let chains to combine parsing, positivity check, and evenness check
    /// in a single flat condition.
    pub fn process(s: &str) -> Option<i32> {
        if let Ok(n) = s.parse::<i32>()
            && n > 0
            && n % 2 == 0
        {
            Some(n * 2)
        } else {
            None
        }
    }
    
    /// Alternative approach using traditional nested if-let (pre-1.88 style).
    pub fn process_nested(s: &str) -> Option<i32> {
        if let Ok(n) = s.parse::<i32>() {
            if n > 0 {
                if n % 2 == 0 {
                    return Some(n * 2);
                }
            }
        }
        None
    }
    
    /// Alternative approach using Option combinators.
    pub fn process_combinators(s: &str) -> Option<i32> {
        s.parse::<i32>()
            .ok()
            .filter(|&n| n > 0)
            .filter(|&n| n % 2 == 0)
            .map(|n| n * 2)
    }
    
    /// Configuration with optional host and port.
    #[derive(Debug, Clone)]
    pub struct Config {
        pub host: Option<String>,
        pub port: Option<u16>,
    }
    
    impl Config {
        pub fn new(host: Option<String>, port: Option<u16>) -> Self {
            Self { host, port }
        }
    }
    
    /// Create an address string from config using let chains.
    ///
    /// Validates that both host and port exist and are valid.
    pub fn make_addr(cfg: &Config) -> Option<String> {
        if let Some(ref host) = cfg.host
            && let Some(port) = cfg.port
            && !host.is_empty()
            && port > 0
        {
            Some(format!("{}:{}", host, port))
        } else {
            None
        }
    }
    
    /// Alternative using Option::zip and filter.
    pub fn make_addr_combinators(cfg: &Config) -> Option<String> {
        cfg.host
            .as_ref()
            .zip(cfg.port)
            .filter(|(host, port)| !host.is_empty() && *port > 0)
            .map(|(host, port)| format!("{}:{}", host, port))
    }
    
    /// Find the first positive even number in a slice of string representations.
    pub fn first_positive_even(data: &[&str]) -> Option<i32> {
        for &s in data {
            if let Ok(n) = s.parse::<i32>()
                && n > 0
                && n % 2 == 0
            {
                return Some(n);
            }
        }
        None
    }
    
    /// Alternative using iterators.
    pub fn first_positive_even_iter(data: &[&str]) -> Option<i32> {
        data.iter()
            .filter_map(|s| s.parse::<i32>().ok())
            .find(|&n| n > 0 && n % 2 == 0)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_process_valid_positive_even() {
            assert_eq!(process("4"), Some(8));
            assert_eq!(process("8"), Some(16));
            assert_eq!(process("100"), Some(200));
        }
    
        #[test]
        fn test_process_negative() {
            assert_eq!(process("-2"), None);
            assert_eq!(process("-4"), None);
        }
    
        #[test]
        fn test_process_odd() {
            assert_eq!(process("3"), None);
            assert_eq!(process("7"), None);
        }
    
        #[test]
        fn test_process_invalid_string() {
            assert_eq!(process("abc"), None);
            assert_eq!(process(""), None);
            assert_eq!(process("4.5"), None);
        }
    
        #[test]
        fn test_process_approaches_equivalent() {
            let test_cases = ["4", "-2", "3", "abc", "8", "0", "10"];
            for s in test_cases {
                assert_eq!(process(s), process_nested(s), "Mismatch for input: {}", s);
                assert_eq!(
                    process(s),
                    process_combinators(s),
                    "Mismatch for input: {}",
                    s
                );
            }
        }
    
        #[test]
        fn test_make_addr_valid() {
            let cfg = Config::new(Some("localhost".into()), Some(8080));
            assert_eq!(make_addr(&cfg), Some("localhost:8080".to_string()));
        }
    
        #[test]
        fn test_make_addr_empty_host() {
            let cfg = Config::new(Some("".into()), Some(8080));
            assert_eq!(make_addr(&cfg), None);
        }
    
        #[test]
        fn test_make_addr_missing_fields() {
            let cfg1 = Config::new(None, Some(80));
            let cfg2 = Config::new(Some("host".into()), None);
            assert_eq!(make_addr(&cfg1), None);
            assert_eq!(make_addr(&cfg2), None);
        }
    
        #[test]
        fn test_make_addr_zero_port() {
            let cfg = Config::new(Some("localhost".into()), Some(0));
            assert_eq!(make_addr(&cfg), None);
        }
    
        #[test]
        fn test_make_addr_approaches_equivalent() {
            let configs = [
                Config::new(Some("localhost".into()), Some(8080)),
                Config::new(Some("".into()), Some(8080)),
                Config::new(None, Some(80)),
                Config::new(Some("host".into()), None),
            ];
            for cfg in &configs {
                assert_eq!(make_addr(cfg), make_addr_combinators(cfg));
            }
        }
    
        #[test]
        fn test_first_positive_even() {
            assert_eq!(first_positive_even(&["x", "3", "-4", "6", "8"]), Some(6));
            assert_eq!(first_positive_even(&["1", "3", "5"]), None);
            assert_eq!(first_positive_even(&["-2", "-4"]), None);
            assert_eq!(first_positive_even(&["2"]), Some(2));
        }
    
        #[test]
        fn test_first_positive_even_approaches_equivalent() {
            let test_cases: &[&[&str]] = &[
                &["x", "3", "-4", "6", "8"],
                &["1", "3", "5"],
                &["-2", "-4"],
                &["2"],
                &[],
            ];
            for data in test_cases {
                assert_eq!(first_positive_even(data), first_positive_even_iter(data));
            }
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_process_valid_positive_even() {
            assert_eq!(process("4"), Some(8));
            assert_eq!(process("8"), Some(16));
            assert_eq!(process("100"), Some(200));
        }
    
        #[test]
        fn test_process_negative() {
            assert_eq!(process("-2"), None);
            assert_eq!(process("-4"), None);
        }
    
        #[test]
        fn test_process_odd() {
            assert_eq!(process("3"), None);
            assert_eq!(process("7"), None);
        }
    
        #[test]
        fn test_process_invalid_string() {
            assert_eq!(process("abc"), None);
            assert_eq!(process(""), None);
            assert_eq!(process("4.5"), None);
        }
    
        #[test]
        fn test_process_approaches_equivalent() {
            let test_cases = ["4", "-2", "3", "abc", "8", "0", "10"];
            for s in test_cases {
                assert_eq!(process(s), process_nested(s), "Mismatch for input: {}", s);
                assert_eq!(
                    process(s),
                    process_combinators(s),
                    "Mismatch for input: {}",
                    s
                );
            }
        }
    
        #[test]
        fn test_make_addr_valid() {
            let cfg = Config::new(Some("localhost".into()), Some(8080));
            assert_eq!(make_addr(&cfg), Some("localhost:8080".to_string()));
        }
    
        #[test]
        fn test_make_addr_empty_host() {
            let cfg = Config::new(Some("".into()), Some(8080));
            assert_eq!(make_addr(&cfg), None);
        }
    
        #[test]
        fn test_make_addr_missing_fields() {
            let cfg1 = Config::new(None, Some(80));
            let cfg2 = Config::new(Some("host".into()), None);
            assert_eq!(make_addr(&cfg1), None);
            assert_eq!(make_addr(&cfg2), None);
        }
    
        #[test]
        fn test_make_addr_zero_port() {
            let cfg = Config::new(Some("localhost".into()), Some(0));
            assert_eq!(make_addr(&cfg), None);
        }
    
        #[test]
        fn test_make_addr_approaches_equivalent() {
            let configs = [
                Config::new(Some("localhost".into()), Some(8080)),
                Config::new(Some("".into()), Some(8080)),
                Config::new(None, Some(80)),
                Config::new(Some("host".into()), None),
            ];
            for cfg in &configs {
                assert_eq!(make_addr(cfg), make_addr_combinators(cfg));
            }
        }
    
        #[test]
        fn test_first_positive_even() {
            assert_eq!(first_positive_even(&["x", "3", "-4", "6", "8"]), Some(6));
            assert_eq!(first_positive_even(&["1", "3", "5"]), None);
            assert_eq!(first_positive_even(&["-2", "-4"]), None);
            assert_eq!(first_positive_even(&["2"]), Some(2));
        }
    
        #[test]
        fn test_first_positive_even_approaches_equivalent() {
            let test_cases: &[&[&str]] = &[
                &["x", "3", "-4", "6", "8"],
                &["1", "3", "5"],
                &["-2", "-4"],
                &["2"],
                &[],
            ];
            for data in test_cases {
                assert_eq!(first_positive_even(data), first_positive_even_iter(data));
            }
        }
    }

    Deep Comparison

    OCaml vs Rust: Let Chains

    Chained Validation

    OCaml (using let* monadic binding)

    let (let*) = Option.bind
    
    let process s =
      let* n = (try Some(int_of_string s) with _ -> None) in
      let* _ = (if n > 0 then Some () else None) in
      let* _ = (if n mod 2 = 0 then Some () else None) in
      Some (n * 2)
    

    Rust (let chains - Rust 1.88+)

    fn process(s: &str) -> Option<i32> {
        if let Ok(n) = s.parse::<i32>()
            && n > 0
            && n % 2 == 0
        {
            Some(n * 2)
        } else {
            None
        }
    }
    

    Key Differences

    AspectOCamlRust
    Syntaxlet* x = expr in ...if let pattern = expr && cond
    MechanismMonadic binding (requires let* definition)Built-in syntax
    Boolean conditionsRequire wrapping in Some()/NoneDirect && condition
    Multiple bindingsEach needs let* x = ...&& let pattern = expr
    ScopeInside the monadic chainBody of the if block
    FallbackImplicitly returns NoneExplicit else branch

    Multiple Pattern Bindings

    OCaml

    let make_addr cfg =
      let* host = cfg.host in
      let* port = cfg.port in
      let* _ = if String.length host > 0 then Some () else None in
      let* _ = if port > 0 then Some () else None in
      Some (host ^ ":" ^ string_of_int port)
    

    Rust

    fn make_addr(cfg: &Config) -> Option<String> {
        if let Some(ref host) = cfg.host
            && let Some(port) = cfg.port
            && !host.is_empty()
            && port > 0
        {
            Some(format!("{}:{}", host, port))
        } else {
            None
        }
    }
    

    When to Use Each

    Rust let chains are ideal when:

  • • Combining pattern matching with boolean guards
  • • Avoiding nested if-let pyramids
  • • Working with multiple Option/Result unpacking in conditions
  • *OCaml let is ideal when:**

  • • You already have monadic infrastructure
  • • Building longer computation chains
  • • Want early return semantics built into the monad
  • Exercises

  • Config extraction: Write fn get_server_addr(config: &Config) -> Option<String> using let chains to extract and validate host, port, and max_conn from an Option<DbConfig>.
  • Multi-parse: Implement fn parse_coord_pair(s: &str) -> Option<(f64, f64)> using let chains to split on comma, parse both parts, and check they are in valid range.
  • Pre-1.88 equivalent: Rewrite the process function using nested if let and explain why let chains improve readability — count the nesting levels.
  • Open Source Repos