ExamplesBy LevelBy TopicLearning Paths
302 Intermediate

302: Option::transpose() — Collecting Optional Results

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "302: Option::transpose() — Collecting Optional Results" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. HashMap lookups return `Option<&V>`. Key difference from OCaml: 1. **Ergonomics**: Rust's `transpose()` condenses the three

Tutorial

The Problem

HashMap lookups return Option<&V>. Parsing the value returns Result<T, E>. The combination is Option<Result<T, E>> — but most downstream code wants Result<Option<T>, E>. The Option::transpose() method handles this conversion. A closely related use case is collecting a Vec<Option<Result<T, E>>> where None means "absent" and Err means "failed to parse", and both need to be handled cleanly.

🎯 Learning Outcomes

  • • Use Option<Result<T, E>>::transpose() to convert to Result<Option<T>, E>
  • • Apply this to map lookups followed by value parsing
  • • Filter out None values while propagating Err from a mixed Vec
  • • Understand the semantics: None becomes Ok(None), Some(Ok(v)) becomes Ok(Some(v)), Some(Err(e)) becomes Err(e)
  • Code Example

    #![allow(clippy::all)]
    //! # Option::transpose() — Collecting Optional Results
    //!
    //! Convert `Option<Result<T, E>>` into `Result<Option<T>, E>`.
    
    use std::collections::HashMap;
    
    /// Lookup a key and parse its value
    pub fn lookup_and_parse(
        map: &HashMap<&str, &str>,
        key: &str,
    ) -> Result<Option<i32>, std::num::ParseIntError> {
        map.get(key).map(|s| s.parse::<i32>()).transpose()
    }
    
    /// Filter and parse optional values
    pub fn parse_optional_values(
        inputs: Vec<Option<&str>>,
    ) -> Result<Vec<i32>, std::num::ParseIntError> {
        inputs
            .into_iter()
            .filter_map(|opt| opt.map(|s| s.parse::<i32>()))
            .collect::<Result<Vec<_>, _>>()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_some_ok_transpose() {
            let v: Option<Result<i32, &str>> = Some(Ok(5));
            assert_eq!(v.transpose(), Ok(Some(5)));
        }
    
        #[test]
        fn test_some_err_transpose() {
            let v: Option<Result<i32, &str>> = Some(Err("fail"));
            assert_eq!(v.transpose(), Err("fail"));
        }
    
        #[test]
        fn test_none_transpose() {
            let v: Option<Result<i32, &str>> = None;
            assert_eq!(v.transpose(), Ok(None));
        }
    
        #[test]
        fn test_lookup_found() {
            let mut map = HashMap::new();
            map.insert("port", "8080");
            assert_eq!(lookup_and_parse(&map, "port").unwrap(), Some(8080));
        }
    
        #[test]
        fn test_lookup_missing() {
            let map: HashMap<&str, &str> = HashMap::new();
            assert_eq!(lookup_and_parse(&map, "port").unwrap(), None);
        }
    
        #[test]
        fn test_lookup_invalid() {
            let mut map = HashMap::new();
            map.insert("port", "bad");
            assert!(lookup_and_parse(&map, "port").is_err());
        }
    
        #[test]
        fn test_parse_optional_values() {
            let inputs = vec![Some("1"), None, Some("2")];
            let result = parse_optional_values(inputs);
            assert_eq!(result.unwrap(), vec![1, 2]);
        }
    }

    Key Differences

  • Ergonomics: Rust's transpose() condenses the three-way match into a single method call; OCaml requires explicit nested pattern matching.
  • Type system: Rust encodes the transformation in the type system — the compiler rejects incorrect applications.
  • filter_map interaction: filter_map(|opt| opt.map(|s| s.parse::<i32>()).transpose()) elegantly handles None-skip and Err-propagate in one expression.
  • collect integration: iter.filter_map(opt_result).collect::<Result<Vec<_>, _>>() combines option filtering with result collection cleanly.
  • OCaml Approach

    OCaml requires explicit pattern matching for this transformation:

    let lookup_and_parse map key =
      match Hashtbl.find_opt map key with
      | None -> Ok None
      | Some s -> match int_of_string_opt s with
        | None -> Error ("invalid: " ^ s)
        | Some n -> Ok (Some n)
    

    Full Source

    #![allow(clippy::all)]
    //! # Option::transpose() — Collecting Optional Results
    //!
    //! Convert `Option<Result<T, E>>` into `Result<Option<T>, E>`.
    
    use std::collections::HashMap;
    
    /// Lookup a key and parse its value
    pub fn lookup_and_parse(
        map: &HashMap<&str, &str>,
        key: &str,
    ) -> Result<Option<i32>, std::num::ParseIntError> {
        map.get(key).map(|s| s.parse::<i32>()).transpose()
    }
    
    /// Filter and parse optional values
    pub fn parse_optional_values(
        inputs: Vec<Option<&str>>,
    ) -> Result<Vec<i32>, std::num::ParseIntError> {
        inputs
            .into_iter()
            .filter_map(|opt| opt.map(|s| s.parse::<i32>()))
            .collect::<Result<Vec<_>, _>>()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_some_ok_transpose() {
            let v: Option<Result<i32, &str>> = Some(Ok(5));
            assert_eq!(v.transpose(), Ok(Some(5)));
        }
    
        #[test]
        fn test_some_err_transpose() {
            let v: Option<Result<i32, &str>> = Some(Err("fail"));
            assert_eq!(v.transpose(), Err("fail"));
        }
    
        #[test]
        fn test_none_transpose() {
            let v: Option<Result<i32, &str>> = None;
            assert_eq!(v.transpose(), Ok(None));
        }
    
        #[test]
        fn test_lookup_found() {
            let mut map = HashMap::new();
            map.insert("port", "8080");
            assert_eq!(lookup_and_parse(&map, "port").unwrap(), Some(8080));
        }
    
        #[test]
        fn test_lookup_missing() {
            let map: HashMap<&str, &str> = HashMap::new();
            assert_eq!(lookup_and_parse(&map, "port").unwrap(), None);
        }
    
        #[test]
        fn test_lookup_invalid() {
            let mut map = HashMap::new();
            map.insert("port", "bad");
            assert!(lookup_and_parse(&map, "port").is_err());
        }
    
        #[test]
        fn test_parse_optional_values() {
            let inputs = vec![Some("1"), None, Some("2")];
            let result = parse_optional_values(inputs);
            assert_eq!(result.unwrap(), vec![1, 2]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_some_ok_transpose() {
            let v: Option<Result<i32, &str>> = Some(Ok(5));
            assert_eq!(v.transpose(), Ok(Some(5)));
        }
    
        #[test]
        fn test_some_err_transpose() {
            let v: Option<Result<i32, &str>> = Some(Err("fail"));
            assert_eq!(v.transpose(), Err("fail"));
        }
    
        #[test]
        fn test_none_transpose() {
            let v: Option<Result<i32, &str>> = None;
            assert_eq!(v.transpose(), Ok(None));
        }
    
        #[test]
        fn test_lookup_found() {
            let mut map = HashMap::new();
            map.insert("port", "8080");
            assert_eq!(lookup_and_parse(&map, "port").unwrap(), Some(8080));
        }
    
        #[test]
        fn test_lookup_missing() {
            let map: HashMap<&str, &str> = HashMap::new();
            assert_eq!(lookup_and_parse(&map, "port").unwrap(), None);
        }
    
        #[test]
        fn test_lookup_invalid() {
            let mut map = HashMap::new();
            map.insert("port", "bad");
            assert!(lookup_and_parse(&map, "port").is_err());
        }
    
        #[test]
        fn test_parse_optional_values() {
            let inputs = vec![Some("1"), None, Some("2")];
            let result = parse_optional_values(inputs);
            assert_eq!(result.unwrap(), vec![1, 2]);
        }
    }

    Deep Comparison

    Option::transpose()

    See 301-result-transpose for comparison.

    Exercises

  • Parse a Vec<Option<&str>> where None means "use default 0" and Some("x") should propagate as an error, using transpose() and unwrap_or.
  • Implement a function that reads an optional HTTP header value and parses it as a number, returning Ok(None) if absent.
  • Collect a Vec<Option<Result<i32, E>>> into a Result<Vec<i32>, E>, skipping None values and short-circuiting on the first Err.
  • Open Source Repos