ExamplesBy LevelBy TopicLearning Paths
292 Intermediate

292: Option Combinators

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "292: Option Combinators" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Null pointer errors are the "billion dollar mistake" — unhandled absence of a value causing runtime crashes. Key difference from OCaml: 1. **Naming**: Rust uses `and_then` for monadic bind; OCaml uses `Option.bind` and `let*` syntax.

Tutorial

The Problem

Null pointer errors are the "billion dollar mistake" — unhandled absence of a value causing runtime crashes. Rust's Option<T> encodes optionality in the type system, and its combinator methods enable composing operations on optional values without null checks or sentinel values. This mirrors OCaml's option type and Haskell's Maybe monad — the foundational functional programming approach to nullable values.

🎯 Learning Outcomes

  • • Use map() to transform Some(T) while passing None through unchanged
  • • Use filter() to conditionally discard a Some value based on a predicate
  • • Chain optional lookups with and_then() — the monadic bind for Option
  • • Provide defaults with unwrap_or(), unwrap_or_else(), and unwrap_or_default()
  • Code Example

    let doubled = Some(5).map(|x| x * 2);
    // Some(10)

    Key Differences

  • Naming: Rust uses and_then for monadic bind; OCaml uses Option.bind and let* syntax.
  • Filter: Rust provides Option::filter() directly; OCaml requires Option.bind (fun x -> if pred x then Some x else None).
  • Defaults: Rust has three variants: unwrap_or(val) (eager), unwrap_or_else(f) (lazy), unwrap_or_default() (type's Default).
  • Zipping: Option::zip(other) combines two options into a pair — no OCaml equivalent without manual matching.
  • OCaml Approach

    OCaml's Option module provides Option.map, Option.bind, and Option.fold. The let* syntax (OCaml 4.08+) desugars to Option.bind:

    let parse_and_sqrt s =
      let* x = float_of_string_opt s in
      if x >= 0.0 then Some (sqrt x) else None
    

    Full Source

    #![allow(clippy::all)]
    //! # Option Combinators
    //!
    //! Work with optional values using `.map()`, `.filter()`, `.and_then()`, and `.unwrap_or()`.
    
    /// Safe square root - returns None for negative inputs
    pub fn safe_sqrt(x: f64) -> Option<f64> {
        if x >= 0.0 {
            Some(x.sqrt())
        } else {
            None
        }
    }
    
    /// Parse and compute square root
    pub fn parse_and_sqrt(s: &str) -> Option<f64> {
        s.parse::<f64>().ok().and_then(safe_sqrt)
    }
    
    /// Map an optional value
    pub fn double_option(opt: Option<i32>) -> Option<i32> {
        opt.map(|x| x * 2)
    }
    
    /// Filter by predicate
    pub fn filter_even(opt: Option<i32>) -> Option<i32> {
        opt.filter(|&x| x % 2 == 0)
    }
    
    /// Get with default
    pub fn get_or_default(opt: Option<i32>, default: i32) -> i32 {
        opt.unwrap_or(default)
    }
    
    /// Get with lazy default
    pub fn get_or_compute<F>(opt: Option<i32>, f: F) -> i32
    where
        F: FnOnce() -> i32,
    {
        opt.unwrap_or_else(f)
    }
    
    /// Chain operations
    pub fn chain_operations(s: &str) -> Option<i32> {
        s.parse::<i32>().ok().filter(|&x| x > 0).map(|x| x * x)
    }
    
    /// Use or for fallback Option
    pub fn first_valid(a: Option<i32>, b: Option<i32>) -> Option<i32> {
        a.or(b)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_map_some() {
            assert_eq!(Some(5i32).map(|x| x * 2), Some(10));
        }
    
        #[test]
        fn test_map_none() {
            assert_eq!(None::<i32>.map(|x| x * 2), None);
        }
    
        #[test]
        fn test_filter_pass() {
            assert_eq!(filter_even(Some(4)), Some(4));
        }
    
        #[test]
        fn test_filter_fail() {
            assert_eq!(filter_even(Some(3)), None);
        }
    
        #[test]
        fn test_and_then_chain() {
            let result = parse_and_sqrt("4.0");
            assert!((result.unwrap() - 2.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_and_then_none() {
            assert_eq!(parse_and_sqrt("invalid"), None);
        }
    
        #[test]
        fn test_and_then_negative() {
            assert_eq!(parse_and_sqrt("-4.0"), None);
        }
    
        #[test]
        fn test_or_default() {
            assert_eq!(get_or_default(None, 42), 42);
            assert_eq!(get_or_default(Some(10), 42), 10);
        }
    
        #[test]
        fn test_or() {
            assert_eq!(first_valid(None, Some(5)), Some(5));
            assert_eq!(first_valid(Some(3), Some(5)), Some(3));
        }
    
        #[test]
        fn test_chain_operations() {
            assert_eq!(chain_operations("5"), Some(25));
            assert_eq!(chain_operations("-5"), None);
            assert_eq!(chain_operations("abc"), None);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_map_some() {
            assert_eq!(Some(5i32).map(|x| x * 2), Some(10));
        }
    
        #[test]
        fn test_map_none() {
            assert_eq!(None::<i32>.map(|x| x * 2), None);
        }
    
        #[test]
        fn test_filter_pass() {
            assert_eq!(filter_even(Some(4)), Some(4));
        }
    
        #[test]
        fn test_filter_fail() {
            assert_eq!(filter_even(Some(3)), None);
        }
    
        #[test]
        fn test_and_then_chain() {
            let result = parse_and_sqrt("4.0");
            assert!((result.unwrap() - 2.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_and_then_none() {
            assert_eq!(parse_and_sqrt("invalid"), None);
        }
    
        #[test]
        fn test_and_then_negative() {
            assert_eq!(parse_and_sqrt("-4.0"), None);
        }
    
        #[test]
        fn test_or_default() {
            assert_eq!(get_or_default(None, 42), 42);
            assert_eq!(get_or_default(Some(10), 42), 10);
        }
    
        #[test]
        fn test_or() {
            assert_eq!(first_valid(None, Some(5)), Some(5));
            assert_eq!(first_valid(Some(3), Some(5)), Some(3));
        }
    
        #[test]
        fn test_chain_operations() {
            assert_eq!(chain_operations("5"), Some(25));
            assert_eq!(chain_operations("-5"), None);
            assert_eq!(chain_operations("abc"), None);
        }
    }

    Deep Comparison

    OCaml vs Rust: Option Combinators

    Pattern 1: Map Some Value

    OCaml

    let some_5 = Some 5 in
    let mapped = Option.map (fun x -> x * 2) some_5
    (* Some 10 *)
    

    Rust

    let doubled = Some(5).map(|x| x * 2);
    // Some(10)
    

    Pattern 2: Chain Optional Operations

    OCaml

    let safe_div x y = if y = 0 then None else Some (x / y) in
    let result = Option.bind some_5 (fun n -> safe_div 10 n)
    

    Rust

    fn safe_div(x: i32, y: i32) -> Option<i32> {
        if y == 0 { None } else { Some(x / y) }
    }
    let result = Some(5).and_then(|n| safe_div(10, n));
    

    Pattern 3: Filter by Predicate

    OCaml

    let even = Option.filter (fun x -> x mod 2 = 0) (Some 6)
    (* Some 6 *)
    

    Rust

    let even = Some(6).filter(|&x| x % 2 == 0);
    // Some(6)
    

    Key Differences

    ConceptOCamlRust
    Map SomeOption.map f optopt.map(f)
    Chain optionalOption.bind opt fopt.and_then(f)
    FilterOption.filter pred optopt.filter(pred)
    Default valueOption.value ~default optopt.unwrap_or(default)
    Lazy fallbackOption.value_or_thunkopt.unwrap_or_else(f)

    Exercises

  • Chain three Option-returning lookups (user → profile → avatar URL) using and_then(), returning None if any step fails.
  • Implement a safe_divide(a: f64, b: f64) -> Option<f64> and compose it with filter(|&x| x.is_finite()) using only combinators.
  • Convert between Option<Result<T, E>> and Result<Option<T>, E> using transpose() and verify the two directions invert each other.
  • Open Source Repos