301: Result::transpose() — Flipping Nested Types
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
Result<Option<T>, E>::transpose() → Option<Result<T, E>>Option<Result<T, E>>::transpose() → Result<Option<T>, E>transpose() to convert between Option<Result<_,_>> and Result<Option<_>,_>transpose() after mapping over an Option to parse a valueCode 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() // => NoneKey Differences
transpose() as a standard method on both Option and Result; OCaml requires manual pattern matching.transpose() makes it possible to use collect::<Result<Vec<Option<_>>, _>>() patterns cleanly.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);
}
}#[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
| Input | Result |
|---|---|
Ok(Some(v)) | Some(Ok(v)) |
Ok(None) | None |
Err(e) | Some(Err(e)) |
| Concept | OCaml | Rust |
|---|---|---|
| Method | Manual match | .transpose() |
| Bidirectional | Two functions | Same method on both types |
| Use case | Composing Option and Result | Same |
Exercises
Vec<Option<&str>> of optional number strings into Result<Vec<Option<i32>>, E> using map, transpose, and collect.Option<&str>) and parses them into a struct, using transpose() for each field.opt.transpose() and result.transpose() are inverses by applying both in sequence and verifying the result.