ExamplesBy LevelBy TopicLearning Paths
293 Intermediate

293: The ? Operator

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "293: The ? Operator" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Chaining fallible operations with `and_then()` is composable but visually noisy when there are many sequential steps. Key difference from OCaml: 1. **Auto

Tutorial

The Problem

Chaining fallible operations with and_then() is composable but visually noisy when there are many sequential steps. The ? operator provides syntactic sugar for early return on failure: expr? desugars to match expr { Ok(v) => v, Err(e) => return Err(e.into()) }. This makes error propagation code read like imperative code while retaining the type safety of explicit Result types. It is Rust's equivalent of OCaml's let* syntax and Haskell's do notation.

🎯 Learning Outcomes

  • • Understand ? as desugaring to early-return on Err (or None)
  • • Recognize that ? calls From::from(e) on the error — enabling automatic type conversion
  • • Use ? in functions returning Result to chain multiple fallible operations
  • • Understand when to use ? vs and_then(): sequential steps vs branching/nested logic
  • Code Example

    fn parse_and_add(s1: &str, s2: &str) -> Result<i32, ParseError> {
        let a = s1.parse::<i32>()?;
        let b = s2.parse::<i32>()?;
        Ok(a + b)
    }

    Key Differences

  • Auto-conversion: Rust's ? calls From::from() on the error automatically; OCaml's let* requires the error type to already unify.
  • Works on both: Rust's ? works on both Result and Option in the same function returning Option; OCaml has separate bind for each.
  • Syntactic position: ? is a postfix operator in Rust; let* is a prefix binding form in OCaml.
  • Propagation level: ? always returns from the enclosing function; and_then can propagate within an expression without returning.
  • OCaml Approach

    OCaml's let* binding (4.08+) is the exact equivalent — it desugars let* x = expr in rest to Result.bind expr (fun x -> rest):

    let process s divisor =
      let* n = int_of_string_opt s |> Option.to_result ~none:`NotANumber in
      if n < 0 then Error `Negative
      else Ok (n / divisor * 2)
    

    OCaml does not perform automatic error type conversion at let* — the error type must already match.

    Full Source

    #![allow(clippy::all)]
    //! # The ? Operator
    //!
    //! `?` desugars to match + return Err(e.into()), enabling clean error propagation.
    
    use std::fmt;
    use std::num::ParseIntError;
    
    #[derive(Debug, PartialEq)]
    pub enum AppError {
        Parse(String),
        DivByZero,
        NegativeInput,
    }
    
    impl fmt::Display for AppError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                AppError::Parse(e) => write!(f, "parse error: {}", e),
                AppError::DivByZero => write!(f, "division by zero"),
                AppError::NegativeInput => write!(f, "negative input"),
            }
        }
    }
    
    impl From<ParseIntError> for AppError {
        fn from(e: ParseIntError) -> Self {
            AppError::Parse(e.to_string())
        }
    }
    
    /// Parse a string as a positive integer
    pub fn parse_positive(s: &str) -> Result<u32, AppError> {
        let n: i32 = s.parse()?; // ? auto-converts ParseIntError via From
        if n < 0 {
            Err(AppError::NegativeInput)
        } else {
            Ok(n as u32)
        }
    }
    
    /// Safe division returning error on division by zero
    pub fn safe_div(a: u32, b: u32) -> Result<u32, AppError> {
        if b == 0 {
            Err(AppError::DivByZero)
        } else {
            Ok(a / b)
        }
    }
    
    /// Compute using chain of ? operators
    pub fn compute(a_str: &str, b_str: &str) -> Result<u32, AppError> {
        let a = parse_positive(a_str)?;
        let b = parse_positive(b_str)?;
        let result = safe_div(a, b)?;
        Ok(result * 2)
    }
    
    /// ? on Option - returns None early
    pub fn find_double(v: &[i32], target: i32) -> Option<i32> {
        let idx = v.iter().position(|&x| x == target)?;
        let val = v.get(idx)?;
        Some(val * 2)
    }
    
    /// Chain multiple optional operations
    pub fn parse_and_lookup(s: &str, map: &std::collections::HashMap<i32, &str>) -> Option<String> {
        let n = s.parse::<i32>().ok()?;
        let value = map.get(&n)?;
        Some(value.to_uppercase())
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::collections::HashMap;
    
        #[test]
        fn test_compute_success() {
            assert_eq!(compute("20", "4"), Ok(10));
        }
    
        #[test]
        fn test_compute_parse_error() {
            assert!(matches!(compute("abc", "4"), Err(AppError::Parse(_))));
        }
    
        #[test]
        fn test_compute_div_zero() {
            assert!(matches!(compute("20", "0"), Err(AppError::DivByZero)));
        }
    
        #[test]
        fn test_compute_negative() {
            assert!(matches!(compute("-5", "2"), Err(AppError::NegativeInput)));
        }
    
        #[test]
        fn test_question_mark_option_found() {
            let v = [1i32, 2, 3];
            assert_eq!(find_double(&v, 2), Some(4));
        }
    
        #[test]
        fn test_question_mark_option_not_found() {
            let v = [1i32, 2, 3];
            assert_eq!(find_double(&v, 9), None);
        }
    
        #[test]
        fn test_parse_and_lookup() {
            let mut map = HashMap::new();
            map.insert(1, "hello");
            map.insert(2, "world");
            assert_eq!(parse_and_lookup("1", &map), Some("HELLO".to_string()));
            assert_eq!(parse_and_lookup("99", &map), None);
            assert_eq!(parse_and_lookup("abc", &map), None);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::collections::HashMap;
    
        #[test]
        fn test_compute_success() {
            assert_eq!(compute("20", "4"), Ok(10));
        }
    
        #[test]
        fn test_compute_parse_error() {
            assert!(matches!(compute("abc", "4"), Err(AppError::Parse(_))));
        }
    
        #[test]
        fn test_compute_div_zero() {
            assert!(matches!(compute("20", "0"), Err(AppError::DivByZero)));
        }
    
        #[test]
        fn test_compute_negative() {
            assert!(matches!(compute("-5", "2"), Err(AppError::NegativeInput)));
        }
    
        #[test]
        fn test_question_mark_option_found() {
            let v = [1i32, 2, 3];
            assert_eq!(find_double(&v, 2), Some(4));
        }
    
        #[test]
        fn test_question_mark_option_not_found() {
            let v = [1i32, 2, 3];
            assert_eq!(find_double(&v, 9), None);
        }
    
        #[test]
        fn test_parse_and_lookup() {
            let mut map = HashMap::new();
            map.insert(1, "hello");
            map.insert(2, "world");
            assert_eq!(parse_and_lookup("1", &map), Some("HELLO".to_string()));
            assert_eq!(parse_and_lookup("99", &map), None);
            assert_eq!(parse_and_lookup("abc", &map), None);
        }
    }

    Deep Comparison

    OCaml vs Rust: The ? Operator

    Pattern 1: Early Return on Error

    OCaml

    let ( let* ) = Result.bind
    
    let parse_and_add s1 s2 =
      let* a = int_of_string_opt s1 |> Option.to_result ~none:"bad" in
      let* b = int_of_string_opt s2 |> Option.to_result ~none:"bad" in
      Ok (a + b)
    

    Rust

    fn parse_and_add(s1: &str, s2: &str) -> Result<i32, ParseError> {
        let a = s1.parse::<i32>()?;
        let b = s2.parse::<i32>()?;
        Ok(a + b)
    }
    

    Pattern 2: Option Early Return

    OCaml

    let ( let* ) = Option.bind
    
    let lookup env key =
      let* value = List.assoc_opt key env in
      let* n = int_of_string_opt value in
      Some (n * 2)
    

    Rust

    fn lookup(map: &HashMap<&str, &str>, key: &str) -> Option<i32> {
        let value = map.get(key)?;
        let n = value.parse::<i32>().ok()?;
        Some(n * 2)
    }
    

    Key Differences

    ConceptOCamlRust
    Syntaxlet* x = expr inlet x = expr?;
    Desugars toResult.bind / Option.bindmatch + return Err(e.into())
    Error conversionManualAutomatic via From trait
    Works on OptionYes (with binding ops)Yes, returns None early
    In closuresYesLimited (must return Result/Option)

    Exercises

  • Rewrite a function that uses three and_then() calls to use ? instead, and verify they produce identical results.
  • Implement two custom error types and use ? to convert between them automatically via impl From.
  • Write a function that uses ? in a main() returning Result<(), Box<dyn Error>> to demonstrate the top-level error propagation pattern.
  • Open Source Repos