ExamplesBy LevelBy TopicLearning Paths
301 Intermediate

301: Result::transpose() — Flipping Nested Types

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "301: Result::transpose() — Flipping Nested Types" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. When mapping over an `Option<&str>` to parse it, the result is `Option<Result<T, E>>` — an option containing a result. Key difference from OCaml: 1. **Standard library**: Rust provides `transpose()` as a standard method on both `Option` and `Result`; OCaml requires manual pattern matching.

Tutorial

The Problem

When mapping over an Option<&str> to parse it, the result is Option<Result<T, E>> — an option containing a result. But many APIs expect Result<Option<T>, E> — a result containing an optional value. The transpose() method converts between these two nested forms, enabling clean composition when optionality and fallibility interact. This is a common need when parsing optional configuration values or handling nullable database fields.

🎯 Learning Outcomes

  • • Understand Result<Option<T>, E>::transpose()Option<Result<T, E>>
  • • Understand Option<Result<T, E>>::transpose()Result<Option<T>, E>
  • • Use transpose() to convert between Option<Result<_,_>> and Result<Option<_>,_>
  • • Apply transpose() after mapping over an Option to parse a value
  • Code Example

    let ok_some: Result<Option<i32>, &str> = Ok(Some(42));
    ok_some.transpose()  // => Some(Ok(42))
    
    let ok_none: Result<Option<i32>, &str> = Ok(None);
    ok_none.transpose()  // => None

    Key Differences

  • Standard library: Rust provides transpose() as a standard method on both Option and Result; OCaml requires manual pattern matching.
  • Nullability interaction: The pattern arises naturally when optional values are parsed — extremely common in config parsing and database nullable field handling.
  • Composition: transpose() makes it possible to use collect::<Result<Vec<Option<_>>, _>>() patterns cleanly.
  • Symmetry: The two directions are inverses: Option::transpose and Result::transpose compose to identity.
  • OCaml Approach

    OCaml does not have a standard transpose function. It is implemented manually as a pattern match:

    let transpose_opt_result = function
      | None -> Ok None
      | Some (Ok v) -> Ok (Some v)
      | Some (Error e) -> Error e
    
    let transpose_result_opt = function
      | Ok None -> None
      | Ok (Some v) -> Some (Ok v)
      | Error e -> Some (Error e)
    

    Full Source

    #![allow(clippy::all)]
    //! # Result::transpose() — Flipping Nested Types
    //!
    //! Convert `Result<Option<T>, E>` into `Option<Result<T, E>>` — or back again.
    
    /// Parse an optional string into a number
    pub fn maybe_parse(s: Option<&str>) -> Result<Option<i32>, std::num::ParseIntError> {
        s.map(|s| s.parse::<i32>()).transpose()
    }
    
    /// Result transpose: Ok(Some(v)) -> Some(Ok(v))
    pub fn result_transpose<T, E>(r: Result<Option<T>, E>) -> Option<Result<T, E>> {
        r.transpose()
    }
    
    /// Option transpose: Some(Ok(v)) -> Ok(Some(v))
    pub fn option_transpose<T, E>(o: Option<Result<T, E>>) -> Result<Option<T>, E> {
        o.transpose()
    }
    
    /// Practical: parse an optional config value
    pub fn parse_optional_config(
        config_val: Option<&str>,
    ) -> Result<Option<i32>, std::num::ParseIntError> {
        config_val.map(|s| s.parse::<i32>()).transpose()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_result_transpose_ok_some() {
            let r: Result<Option<i32>, &str> = Ok(Some(42));
            assert_eq!(r.transpose(), Some(Ok(42)));
        }
    
        #[test]
        fn test_result_transpose_ok_none() {
            let r: Result<Option<i32>, &str> = Ok(None);
            assert_eq!(r.transpose(), None);
        }
    
        #[test]
        fn test_result_transpose_err() {
            let r: Result<Option<i32>, &str> = Err("bad");
            assert_eq!(r.transpose(), Some(Err("bad")));
        }
    
        #[test]
        fn test_option_transpose_some_ok() {
            let o: Option<Result<i32, &str>> = Some(Ok(5));
            assert_eq!(o.transpose(), Ok(Some(5)));
        }
    
        #[test]
        fn test_option_transpose_some_err() {
            let o: Option<Result<i32, &str>> = Some(Err("fail"));
            assert_eq!(o.transpose(), Err("fail"));
        }
    
        #[test]
        fn test_option_transpose_none() {
            let o: Option<Result<i32, &str>> = None;
            assert_eq!(o.transpose(), Ok(None));
        }
    
        #[test]
        fn test_parse_optional_config_some() {
            let result = parse_optional_config(Some("42"));
            assert_eq!(result.unwrap(), Some(42));
        }
    
        #[test]
        fn test_parse_optional_config_none() {
            let result = parse_optional_config(None);
            assert_eq!(result.unwrap(), None);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_result_transpose_ok_some() {
            let r: Result<Option<i32>, &str> = Ok(Some(42));
            assert_eq!(r.transpose(), Some(Ok(42)));
        }
    
        #[test]
        fn test_result_transpose_ok_none() {
            let r: Result<Option<i32>, &str> = Ok(None);
            assert_eq!(r.transpose(), None);
        }
    
        #[test]
        fn test_result_transpose_err() {
            let r: Result<Option<i32>, &str> = Err("bad");
            assert_eq!(r.transpose(), Some(Err("bad")));
        }
    
        #[test]
        fn test_option_transpose_some_ok() {
            let o: Option<Result<i32, &str>> = Some(Ok(5));
            assert_eq!(o.transpose(), Ok(Some(5)));
        }
    
        #[test]
        fn test_option_transpose_some_err() {
            let o: Option<Result<i32, &str>> = Some(Err("fail"));
            assert_eq!(o.transpose(), Err("fail"));
        }
    
        #[test]
        fn test_option_transpose_none() {
            let o: Option<Result<i32, &str>> = None;
            assert_eq!(o.transpose(), Ok(None));
        }
    
        #[test]
        fn test_parse_optional_config_some() {
            let result = parse_optional_config(Some("42"));
            assert_eq!(result.unwrap(), Some(42));
        }
    
        #[test]
        fn test_parse_optional_config_none() {
            let result = parse_optional_config(None);
            assert_eq!(result.unwrap(), None);
        }
    }

    Deep Comparison

    OCaml vs Rust: transpose

    Pattern: Result<Option<T>> to Option<Result<T>>

    Rust

    let ok_some: Result<Option<i32>, &str> = Ok(Some(42));
    ok_some.transpose()  // => Some(Ok(42))
    
    let ok_none: Result<Option<i32>, &str> = Ok(None);
    ok_none.transpose()  // => None
    

    OCaml

    let transpose = function
      | Ok (Some v) -> Some (Ok v)
      | Ok None -> None
      | Error e -> Some (Error e)
    

    Key Differences

    InputResult
    Ok(Some(v))Some(Ok(v))
    Ok(None)None
    Err(e)Some(Err(e))
    ConceptOCamlRust
    MethodManual match.transpose()
    BidirectionalTwo functionsSame method on both types
    Use caseComposing Option and ResultSame

    Exercises

  • Parse a Vec<Option<&str>> of optional number strings into Result<Vec<Option<i32>>, E> using map, transpose, and collect.
  • Write a function that reads optional database fields (represented as Option<&str>) and parses them into a struct, using transpose() for each field.
  • Demonstrate that opt.transpose() and result.transpose() are inverses by applying both in sequence and verifying the result.
  • Open Source Repos