868-traversable-tree — Traversable Tree
Tutorial
The Problem
A Traversable structure extends Foldable by allowing each element to produce an effect, then collecting the results into the same shape. The canonical examples are: validate every node (returning Option<Tree<U>> — None if any node fails), or parse every node (returning Result<Tree<U>, E> — Err on first failure). This is formally the "traverse" operation from the Haskell Traversable typeclass. It is used in compilers for type-checking expression trees, in configuration validators that walk nested structures, and in reactive systems that need all-or-nothing transformation of data trees.
🎯 Learning Outcomes
traverse_option and traverse_result for a binary tree? operator inside recursive tree functions to propagate failuresOption/Result chaining with OCaml's monadic bindCode Example
fn traverse_option<U>(&self, f: &impl Fn(&T) -> Option<U>) -> Option<Tree<U>> {
match self {
Tree::Leaf => Some(Tree::Leaf),
Tree::Node(l, v, r) => {
let l2 = l.traverse_option(f)?; // ? replaces nested match
let v2 = f(v)?;
let r2 = r.traverse_option(f)?;
Some(Tree::node(l2, v2, r2))
}
}
}Key Differences
? inside recursive functions; OCaml uses explicit match or let* monadic bind.Option and Result in a single generic traverse; OCaml can via module functors parameterized on an applicative.&self and must clone subtrees into Some(Tree::node(...)) on success; OCaml reuses GC-managed nodes.OCaml Approach
OCaml lacks the ? shorthand, so traversal is expressed as explicit pattern matches on None/Some and Error/Ok inside each recursive case. OCaml's Traversable typeclass equivalent requires a module functor parameterized over the applicative/monad. In practice, OCaml users write the specific traverse_option and traverse_result directly as shown in example.ml. With the let* syntax (OCaml 4.08+), monadic chains become readable without manual nesting.
Full Source
#![allow(clippy::all)]
// Example 069: Traversable for Binary Tree
// Map over a tree with effects (Option/Result)
#[derive(Debug, PartialEq, Clone)]
enum Tree<T> {
Leaf,
Node(Box<Tree<T>>, T, Box<Tree<T>>),
}
impl<T> Tree<T> {
fn node(l: Tree<T>, v: T, r: Tree<T>) -> Self {
Tree::Node(Box::new(l), v, Box::new(r))
}
// Approach 1: Traverse with Option
fn traverse_option<U>(&self, f: &impl Fn(&T) -> Option<U>) -> Option<Tree<U>> {
match self {
Tree::Leaf => Some(Tree::Leaf),
Tree::Node(l, v, r) => {
let l2 = l.traverse_option(f)?;
let v2 = f(v)?;
let r2 = r.traverse_option(f)?;
Some(Tree::node(l2, v2, r2))
}
}
}
// Approach 2: Traverse with Result
fn traverse_result<U, E>(&self, f: &impl Fn(&T) -> Result<U, E>) -> Result<Tree<U>, E> {
match self {
Tree::Leaf => Ok(Tree::Leaf),
Tree::Node(l, v, r) => {
let l2 = l.traverse_result(f)?;
let v2 = f(v)?;
let r2 = r.traverse_result(f)?;
Ok(Tree::node(l2, v2, r2))
}
}
}
// Approach 3: Pure map (no effect)
fn map<U>(&self, f: &impl Fn(&T) -> U) -> Tree<U> {
match self {
Tree::Leaf => Tree::Leaf,
Tree::Node(l, v, r) => Tree::node(l.map(f), f(v), r.map(f)),
}
}
fn to_vec(&self) -> Vec<&T> {
match self {
Tree::Leaf => vec![],
Tree::Node(l, v, r) => {
let mut result = l.to_vec();
result.push(v);
result.extend(r.to_vec());
result
}
}
}
}
fn safe_double(x: &i32) -> Option<i32> {
if *x > 50 {
None
} else {
Some(x * 2)
}
}
fn parse_positive(x: &i32) -> Result<i32, String> {
if *x > 0 {
Ok(*x)
} else {
Err(format!("Not positive: {}", x))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Tree<i32> {
Tree::node(
Tree::node(Tree::Leaf, 1, Tree::Leaf),
2,
Tree::node(Tree::Leaf, 3, Tree::Leaf),
)
}
#[test]
fn test_traverse_option_success() {
let result = sample().traverse_option(&safe_double);
let expected = Tree::node(
Tree::node(Tree::Leaf, 2, Tree::Leaf),
4,
Tree::node(Tree::Leaf, 6, Tree::Leaf),
);
assert_eq!(result, Some(expected));
}
#[test]
fn test_traverse_option_failure() {
let tree = Tree::node(Tree::node(Tree::Leaf, 10, Tree::Leaf), 60, Tree::Leaf);
assert_eq!(tree.traverse_option(&safe_double), None);
}
#[test]
fn test_traverse_result_success() {
assert_eq!(sample().traverse_result(&parse_positive), Ok(sample()));
}
#[test]
fn test_traverse_result_failure() {
let tree = Tree::node(Tree::Leaf, -1, Tree::Leaf);
assert_eq!(
tree.traverse_result(&parse_positive),
Err("Not positive: -1".into())
);
}
#[test]
fn test_map() {
let doubled = sample().map(&|x| x * 2);
assert_eq!(doubled.to_vec(), vec![&2, &4, &6]);
}
#[test]
fn test_traverse_leaf() {
assert_eq!(
Tree::<i32>::Leaf.traverse_option(&safe_double),
Some(Tree::Leaf)
);
}
}#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Tree<i32> {
Tree::node(
Tree::node(Tree::Leaf, 1, Tree::Leaf),
2,
Tree::node(Tree::Leaf, 3, Tree::Leaf),
)
}
#[test]
fn test_traverse_option_success() {
let result = sample().traverse_option(&safe_double);
let expected = Tree::node(
Tree::node(Tree::Leaf, 2, Tree::Leaf),
4,
Tree::node(Tree::Leaf, 6, Tree::Leaf),
);
assert_eq!(result, Some(expected));
}
#[test]
fn test_traverse_option_failure() {
let tree = Tree::node(Tree::node(Tree::Leaf, 10, Tree::Leaf), 60, Tree::Leaf);
assert_eq!(tree.traverse_option(&safe_double), None);
}
#[test]
fn test_traverse_result_success() {
assert_eq!(sample().traverse_result(&parse_positive), Ok(sample()));
}
#[test]
fn test_traverse_result_failure() {
let tree = Tree::node(Tree::Leaf, -1, Tree::Leaf);
assert_eq!(
tree.traverse_result(&parse_positive),
Err("Not positive: -1".into())
);
}
#[test]
fn test_map() {
let doubled = sample().map(&|x| x * 2);
assert_eq!(doubled.to_vec(), vec![&2, &4, &6]);
}
#[test]
fn test_traverse_leaf() {
assert_eq!(
Tree::<i32>::Leaf.traverse_option(&safe_double),
Some(Tree::Leaf)
);
}
}
Deep Comparison
Comparison: Traversable for Binary Tree
Traverse with Option
OCaml:
let rec traverse_option f = function
| Leaf -> Some Leaf
| Node (l, v, r) ->
match traverse_option f l with
| None -> None
| Some l' -> match f v with
| None -> None
| Some v' -> match traverse_option f r with
| None -> None
| Some r' -> Some (Node (l', v', r'))
Rust (? operator makes it clean!):
fn traverse_option<U>(&self, f: &impl Fn(&T) -> Option<U>) -> Option<Tree<U>> {
match self {
Tree::Leaf => Some(Tree::Leaf),
Tree::Node(l, v, r) => {
let l2 = l.traverse_option(f)?; // ? replaces nested match
let v2 = f(v)?;
let r2 = r.traverse_option(f)?;
Some(Tree::node(l2, v2, r2))
}
}
}
Pure Map (No Effect)
OCaml:
let rec map f = function
| Leaf -> Leaf
| Node (l, v, r) -> Node (map f l, f v, map f r)
Rust:
fn map<U>(&self, f: &impl Fn(&T) -> U) -> Tree<U> {
match self {
Tree::Leaf => Tree::Leaf,
Tree::Node(l, v, r) => Tree::node(l.map(f), f(v), r.map(f)),
}
}
Exercises
traverse_vec that applies f: &T -> Vec<U> to each node, returning a Vec<Tree<U>> with all combinations (cartesian product across nodes).sequence_option function that converts Tree<Option<T>> into Option<Tree<T>> using traverse_option with the identity function.validate_all that collects all Err values into a Vec<E> rather than stopping at the first, using a custom accumulating result type.