302: Option::transpose() — Collecting Optional Results
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
Option<Result<T, E>>::transpose() to convert to Result<Option<T>, E>None values while propagating Err from a mixed VecNone 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
transpose() condenses the three-way match into a single method call; OCaml requires explicit nested pattern matching.filter_map(|opt| opt.map(|s| s.parse::<i32>()).transpose()) elegantly handles None-skip and Err-propagate in one expression.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]);
}
}#[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
Vec<Option<&str>> where None means "use default 0" and Some("x") should propagate as an error, using transpose() and unwrap_or.Ok(None) if absent.Vec<Option<Result<i32, E>>> into a Result<Vec<i32>, E>, skipping None values and short-circuiting on the first Err.