Sequence Monadic
Tutorial
The Problem
sequence converts a collection of monadic values into a monadic collection: Vec<Option<T>> → Option<Vec<T>> or Vec<Result<T,E>> → Result<Vec<T>,E>. While traverse applies a function and sequences simultaneously, sequence is pure rearrangement — it just flips the containers. If you already have a Vec<Option<T>> from mapping over a collection, sequence collects them into Option<Vec<T>>. Rust's Iterator::collect achieves this for Option and Result. The pattern appears when: you have pre-computed individual results and need to combine them, or when working with futures (Vec<Future<T>> → Future<Vec<T>> via futures::future::join_all).
🎯 Learning Outcomes
sequence as a special case of traverse with the identity functionsequence_option(Vec<Option<T>>) -> Option<Vec<T>> using collectsequence_result(Vec<Result<T,E>>) -> Result<Vec<T>,E> using collectjoin_all is sequence for futuresflat_map: flat_map chains and flattens; sequence flips the containerCode Example
fn sequence_option<T>(xs: Vec<Option<T>>) -> Option<Vec<T>> {
xs.into_iter().collect() // That's it!
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| For Option | collect::<Option<Vec<_>>>() | List.fold_right or Option.all |
| For Result | collect::<Result<Vec<_>,_>>() | Same fold pattern |
| One-liner | Yes (built-in FromIterator) | Requires manual fold |
| Futures | futures::join_all | Lwt.all |
| vs. traverse | traverse(xs, id) = sequence | traverse Fun.id xs |
| Short-circuit | First None/Err | Same |
OCaml Approach
OCaml's sequence_option: let sequence xs = List.fold_right (fun x acc -> match x, acc with Some y, Some ys -> Some (y :: ys) | _ -> None) xs (Some []). The sequence_result is analogous with Result. OCaml's Option.all (in some versions) or manual fold implements this. For futures, Lwt.all sequences a list of Lwt promises. The relationship to traverse: sequence xs = traverse Fun.id xs.
Full Source
#![allow(clippy::all)]
// Example 066: Sequence Monadic
// Turn a collection of monadic values into a monadic collection
// Approach 1: sequence for Option using collect
fn sequence_option<T>(xs: Vec<Option<T>>) -> Option<Vec<T>> {
xs.into_iter().collect()
}
// Approach 2: sequence for Result using collect
fn sequence_result<T, E>(xs: Vec<Result<T, E>>) -> Result<Vec<T>, E> {
xs.into_iter().collect()
}
// Approach 3: Manual fold implementation
fn sequence_option_fold<T>(xs: Vec<Option<T>>) -> Option<Vec<T>> {
xs.into_iter().try_fold(Vec::new(), |mut acc, x| {
acc.push(x?);
Some(acc)
})
}
fn sequence_result_fold<T, E>(xs: Vec<Result<T, E>>) -> Result<Vec<T>, E> {
xs.into_iter().try_fold(Vec::new(), |mut acc, x| {
acc.push(x?);
Ok(acc)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sequence_option_all_some() {
assert_eq!(
sequence_option(vec![Some(1), Some(2), Some(3)]),
Some(vec![1, 2, 3])
);
}
#[test]
fn test_sequence_option_with_none() {
assert_eq!(sequence_option(vec![Some(1), None, Some(3)]), None);
}
#[test]
fn test_sequence_option_empty() {
assert_eq!(sequence_option::<i32>(vec![]), Some(vec![]));
}
#[test]
fn test_sequence_result_all_ok() {
assert_eq!(
sequence_result::<i32, String>(vec![Ok(1), Ok(2), Ok(3)]),
Ok(vec![1, 2, 3])
);
}
#[test]
fn test_sequence_result_with_err() {
let rs: Vec<Result<i32, &str>> = vec![Ok(1), Err("e"), Ok(3)];
assert_eq!(sequence_result(rs), Err("e"));
}
#[test]
fn test_fold_versions() {
assert_eq!(
sequence_option_fold(vec![Some(1), Some(2)]),
Some(vec![1, 2])
);
assert_eq!(sequence_option_fold(vec![Some(1), None]), None);
assert_eq!(
sequence_result_fold::<i32, String>(vec![Ok(1), Ok(2)]),
Ok(vec![1, 2])
);
}
#[test]
fn test_practical_parse() {
let parsed: Option<Vec<i32>> = vec!["1", "2", "3"].iter().map(|s| s.parse().ok()).collect();
assert_eq!(parsed, Some(vec![1, 2, 3]));
let parsed2: Option<Vec<i32>> = vec!["1", "bad", "3"]
.iter()
.map(|s| s.parse().ok())
.collect();
assert_eq!(parsed2, None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sequence_option_all_some() {
assert_eq!(
sequence_option(vec![Some(1), Some(2), Some(3)]),
Some(vec![1, 2, 3])
);
}
#[test]
fn test_sequence_option_with_none() {
assert_eq!(sequence_option(vec![Some(1), None, Some(3)]), None);
}
#[test]
fn test_sequence_option_empty() {
assert_eq!(sequence_option::<i32>(vec![]), Some(vec![]));
}
#[test]
fn test_sequence_result_all_ok() {
assert_eq!(
sequence_result::<i32, String>(vec![Ok(1), Ok(2), Ok(3)]),
Ok(vec![1, 2, 3])
);
}
#[test]
fn test_sequence_result_with_err() {
let rs: Vec<Result<i32, &str>> = vec![Ok(1), Err("e"), Ok(3)];
assert_eq!(sequence_result(rs), Err("e"));
}
#[test]
fn test_fold_versions() {
assert_eq!(
sequence_option_fold(vec![Some(1), Some(2)]),
Some(vec![1, 2])
);
assert_eq!(sequence_option_fold(vec![Some(1), None]), None);
assert_eq!(
sequence_result_fold::<i32, String>(vec![Ok(1), Ok(2)]),
Ok(vec![1, 2])
);
}
#[test]
fn test_practical_parse() {
let parsed: Option<Vec<i32>> = vec!["1", "2", "3"].iter().map(|s| s.parse().ok()).collect();
assert_eq!(parsed, Some(vec![1, 2, 3]));
let parsed2: Option<Vec<i32>> = vec!["1", "bad", "3"]
.iter()
.map(|s| s.parse().ok())
.collect();
assert_eq!(parsed2, None);
}
}
Deep Comparison
Comparison: Sequence Monadic
Option Sequence
OCaml:
let sequence_option xs =
List.fold_right (fun x acc ->
match x, acc with
| Some y, Some ys -> Some (y :: ys)
| _ -> None
) xs (Some [])
Rust:
fn sequence_option<T>(xs: Vec<Option<T>>) -> Option<Vec<T>> {
xs.into_iter().collect() // That's it!
}
Generic Sequence
OCaml:
let sequence_generic ~bind ~return_ xs =
List.fold_right (fun mx acc ->
bind mx (fun x ->
bind acc (fun xs ->
return_ (x :: xs)))
) xs (return_ [])
Rust (no direct equivalent — collect handles it via FromIterator):
// For Option: xs.into_iter().collect::<Option<Vec<_>>>()
// For Result: xs.into_iter().collect::<Result<Vec<_>, _>>()
// The trait system handles the dispatch
Exercises
sequence_option using try_fold (without collect) to understand the manual accumulation.sequence(map(xs, f)) == traverse(xs, f) with a concrete test.sequence for a Vec<Vec<T>> — what does flipping the containers mean here? (It's transpose.)sequence to collect results from parallel IO operations: generate Vec<Result<T,E>> then sequence.unsequence: Option<Vec<T>> -> Vec<Option<T>> and verify it's the inverse when all elements are Some.