Tree Zipper
Tutorial Video
Text description (accessibility)
This video demonstrates the "Tree Zipper" functional Rust example. Difficulty level: Advanced. Key concepts covered: Trees, Functional Data Structures, Zippers. Implement a *zipper* for binary trees: a data structure that tracks a focused subtree together with a breadcrumb trail back to the root, enabling O(1) local navigation (`go_left`, `go_right`, `go_up`) and functional in-place editing (`set_value`) without mutating the original tree. Key difference from OCaml: 1. **Ownership vs. copying:** OCaml copies the record on each navigation step; Rust
Tutorial
The Problem
Implement a zipper for binary trees: a data structure that tracks a focused
subtree together with a breadcrumb trail back to the root, enabling O(1)
local navigation (go_left, go_right, go_up) and functional in-place
editing (set_value) without mutating the original tree.
🎯 Learning Outcomes
Zipper<T> by value makes navigation safe and allocation-freeto_tree loop where OCaml can express the same idea as a one-liner (match go_up z with None -> z.focus | Some z' -> to_tree z')🦀 The Rust Way
Rust models the same structure with an owned Zipper<T> (focus + Vec<Crumb<T>>).
go_left / go_right / go_up consume the zipper and return Option<Zipper<T>>,
making the state transition explicit in the type. Because go_up moves z, to_tree
cannot read z.focus after the call; instead it checks z.trail.is_empty() first
and loops, which is both idiomatic and tail-call-free in practice.
Code Example
#[derive(Debug, Clone, PartialEq)]
pub enum Tree<T> {
Leaf,
Node(Box<Tree<T>>, T, Box<Tree<T>>),
}
#[derive(Debug, Clone)]
pub enum Crumb<T> {
Left(T, Tree<T>),
Right(Tree<T>, T),
}
#[derive(Debug, Clone)]
pub struct Zipper<T> {
pub focus: Tree<T>,
pub trail: Vec<Crumb<T>>,
}
pub fn go_up<T>(mut z: Zipper<T>) -> Option<Zipper<T>> {
match z.trail.pop() {
None => None,
Some(Crumb::Left(v, r)) => Some(Zipper {
focus: Tree::node(z.focus, v, r),
trail: z.trail,
}),
Some(Crumb::Right(l, v)) => Some(Zipper {
focus: Tree::node(l, v, z.focus),
trail: z.trail,
}),
}
}
// Idiomatic: loop + early return avoids the ownership issue
pub fn to_tree<T>(mut z: Zipper<T>) -> Tree<T> {
loop {
if z.trail.is_empty() {
return z.focus;
}
z = go_up(z).expect("trail was non-empty");
}
}Key Differences
moves it, making it clear that the old zipper is consumed and the new one is returned.
Vec (push/pop), which is more cache-friendly and avoids allocation per step.
to_tree idiom:** OCaml can read z.focus after go_up z in the same match; Rust requires checking z.trail.is_empty() before calling go_up because go_up
consumes z.
Option for navigation — the only difference is syntax (None/Some is identical; OCaml's Option.get vs.
Rust's .expect()).
OCaml Approach
OCaml uses a record { focus: 'a tree; trail: 'a crumb list } with a prepend-cons trail.
Each navigation function returns an option zipper. Because OCaml passes records by
value (copying), go_up can pattern-match on go_up z and still reference z.focus
in the None branch — the compiler has no ownership concern.
Full Source
#![allow(clippy::all)]
/// A binary tree — either a Leaf (empty) or a Node with left subtree, value, right subtree.
#[derive(Debug, Clone, PartialEq)]
pub enum Tree<T> {
Leaf,
Node(Box<Tree<T>>, T, Box<Tree<T>>),
}
impl<T> Tree<T> {
pub fn node(left: Tree<T>, val: T, right: Tree<T>) -> Self {
Tree::Node(Box::new(left), val, Box::new(right))
}
}
/// A crumb records which direction we descended and what we left behind.
/// Left(v, r) — we went left; parent had value v and right subtree r
/// Right(l, v) — we went right; parent had left subtree l and value v
#[derive(Debug, Clone)]
pub enum Crumb<T> {
Left(T, Tree<T>),
Right(Tree<T>, T),
}
/// A zipper: a focused subtree plus the breadcrumb trail back to the root.
/// Navigation is O(1); rebuilding the whole tree is O(depth).
#[derive(Debug, Clone)]
pub struct Zipper<T> {
pub focus: Tree<T>,
pub trail: Vec<Crumb<T>>,
}
/// Wrap a tree in a zipper focused on the root.
pub fn of_tree<T>(tree: Tree<T>) -> Zipper<T> {
Zipper {
focus: tree,
trail: Vec::new(),
}
}
/// Move focus to the left child. Returns None if focused on a Leaf.
pub fn go_left<T>(mut z: Zipper<T>) -> Option<Zipper<T>> {
match z.focus {
Tree::Leaf => None,
Tree::Node(l, v, r) => {
z.trail.push(Crumb::Left(v, *r));
Some(Zipper {
focus: *l,
trail: z.trail,
})
}
}
}
/// Move focus to the right child. Returns None if focused on a Leaf.
pub fn go_right<T>(mut z: Zipper<T>) -> Option<Zipper<T>> {
match z.focus {
Tree::Leaf => None,
Tree::Node(l, v, r) => {
z.trail.push(Crumb::Right(*l, v));
Some(Zipper {
focus: *r,
trail: z.trail,
})
}
}
}
/// Move focus to the parent. Returns None if already at the root.
pub fn go_up<T>(mut z: Zipper<T>) -> Option<Zipper<T>> {
match z.trail.pop() {
None => None,
Some(Crumb::Left(v, r)) => Some(Zipper {
focus: Tree::node(z.focus, v, r),
trail: z.trail,
}),
Some(Crumb::Right(l, v)) => Some(Zipper {
focus: Tree::node(l, v, z.focus),
trail: z.trail,
}),
}
}
/// Replace the value at the current focus (no-op if focused on a Leaf).
pub fn set_value<T>(x: T, z: Zipper<T>) -> Zipper<T> {
match z.focus {
Tree::Leaf => z,
Tree::Node(l, _, r) => Zipper {
focus: Tree::node(*l, x, *r),
trail: z.trail,
},
}
}
/// Climb back to the root and return the reconstructed tree (idiomatic iterative).
///
/// Note: in OCaml `to_tree` can be written as a one-liner because the language
/// lets you read `z.focus` after passing `z` to `go_up` in the same match.
/// In Rust, `go_up` consumes `z` by value, so we must save the focus *before*
/// calling `go_up`, which is clearest expressed as a loop.
pub fn to_tree<T>(mut z: Zipper<T>) -> Tree<T> {
loop {
if z.trail.is_empty() {
return z.focus;
}
z = go_up(z).expect("trail was non-empty");
}
}
/// Climb back to the root — explicit recursion mirrors the OCaml original.
pub fn to_tree_recursive<T>(z: Zipper<T>) -> Tree<T> {
if z.trail.is_empty() {
return z.focus;
}
to_tree_recursive(go_up(z).expect("trail was non-empty"))
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_tree() -> Tree<i32> {
// Node(Node(Leaf,1,Leaf), 2, Node(Leaf,3,Leaf))
Tree::node(
Tree::node(Tree::Leaf, 1, Tree::Leaf),
2,
Tree::node(Tree::Leaf, 3, Tree::Leaf),
)
}
#[test]
fn test_of_tree_is_root() {
let z = of_tree(sample_tree());
assert!(z.trail.is_empty());
}
#[test]
fn test_go_left_moves_focus() {
let z = of_tree(sample_tree());
let z = go_left(z).expect("should have left child");
assert_eq!(z.focus, Tree::node(Tree::Leaf, 1, Tree::Leaf));
assert_eq!(z.trail.len(), 1);
}
#[test]
fn test_go_right_moves_focus() {
let z = of_tree(sample_tree());
let z = go_right(z).expect("should have right child");
assert_eq!(z.focus, Tree::node(Tree::Leaf, 3, Tree::Leaf));
assert_eq!(z.trail.len(), 1);
}
#[test]
fn test_go_left_then_up_returns_root() {
let z = of_tree(sample_tree());
let z = go_left(z).expect("left");
let z = go_up(z).expect("up");
assert_eq!(z.focus, sample_tree());
assert!(z.trail.is_empty());
}
#[test]
fn test_go_right_then_up_returns_root() {
let z = of_tree(sample_tree());
let z = go_right(z).expect("right");
let z = go_up(z).expect("up");
assert_eq!(z.focus, sample_tree());
assert!(z.trail.is_empty());
}
#[test]
fn test_set_value_updates_focused_node() {
let z = of_tree(sample_tree());
let z = go_left(z).expect("left");
let z = set_value(10, z);
assert_eq!(z.focus, Tree::node(Tree::Leaf, 10, Tree::Leaf));
}
#[test]
fn test_set_value_on_leaf_is_noop() {
let z: Zipper<i32> = of_tree(Tree::Leaf);
let z2 = set_value(42, z);
assert_eq!(z2.focus, Tree::Leaf);
}
#[test]
fn test_to_tree_rebuilds_after_edit() {
let z = of_tree(sample_tree());
let z = go_left(z).expect("left");
let z = set_value(10, z);
let result = to_tree(z);
let expected = Tree::node(
Tree::node(Tree::Leaf, 10, Tree::Leaf),
2,
Tree::node(Tree::Leaf, 3, Tree::Leaf),
);
assert_eq!(result, expected);
}
#[test]
fn test_to_tree_recursive_matches_iter() {
let z = of_tree(sample_tree());
let z = go_right(z).expect("right");
let z = set_value(30, z);
let result = to_tree_recursive(z);
let expected = Tree::node(
Tree::node(Tree::Leaf, 1, Tree::Leaf),
2,
Tree::node(Tree::Leaf, 30, Tree::Leaf),
);
assert_eq!(result, expected);
}
#[test]
fn test_go_left_on_leaf_returns_none() {
let z: Zipper<i32> = of_tree(Tree::Leaf);
assert!(go_left(z).is_none());
}
#[test]
fn test_go_up_at_root_returns_none() {
let z = of_tree(sample_tree());
assert!(go_up(z).is_none());
}
#[test]
fn test_deep_navigation_and_edit() {
// Build a 3-level tree: root=5, left subtree root=3, its left child=1
let tree = Tree::node(
Tree::node(Tree::node(Tree::Leaf, 1, Tree::Leaf), 3, Tree::Leaf),
5,
Tree::Leaf,
);
let z = of_tree(tree);
let z = go_left(z).expect("left");
let z = go_left(z).expect("left-left");
let z = set_value(99, z);
let result = to_tree(z);
let expected = Tree::node(
Tree::node(Tree::node(Tree::Leaf, 99, Tree::Leaf), 3, Tree::Leaf),
5,
Tree::Leaf,
);
assert_eq!(result, expected);
}
}#[cfg(test)]
mod tests {
use super::*;
fn sample_tree() -> Tree<i32> {
// Node(Node(Leaf,1,Leaf), 2, Node(Leaf,3,Leaf))
Tree::node(
Tree::node(Tree::Leaf, 1, Tree::Leaf),
2,
Tree::node(Tree::Leaf, 3, Tree::Leaf),
)
}
#[test]
fn test_of_tree_is_root() {
let z = of_tree(sample_tree());
assert!(z.trail.is_empty());
}
#[test]
fn test_go_left_moves_focus() {
let z = of_tree(sample_tree());
let z = go_left(z).expect("should have left child");
assert_eq!(z.focus, Tree::node(Tree::Leaf, 1, Tree::Leaf));
assert_eq!(z.trail.len(), 1);
}
#[test]
fn test_go_right_moves_focus() {
let z = of_tree(sample_tree());
let z = go_right(z).expect("should have right child");
assert_eq!(z.focus, Tree::node(Tree::Leaf, 3, Tree::Leaf));
assert_eq!(z.trail.len(), 1);
}
#[test]
fn test_go_left_then_up_returns_root() {
let z = of_tree(sample_tree());
let z = go_left(z).expect("left");
let z = go_up(z).expect("up");
assert_eq!(z.focus, sample_tree());
assert!(z.trail.is_empty());
}
#[test]
fn test_go_right_then_up_returns_root() {
let z = of_tree(sample_tree());
let z = go_right(z).expect("right");
let z = go_up(z).expect("up");
assert_eq!(z.focus, sample_tree());
assert!(z.trail.is_empty());
}
#[test]
fn test_set_value_updates_focused_node() {
let z = of_tree(sample_tree());
let z = go_left(z).expect("left");
let z = set_value(10, z);
assert_eq!(z.focus, Tree::node(Tree::Leaf, 10, Tree::Leaf));
}
#[test]
fn test_set_value_on_leaf_is_noop() {
let z: Zipper<i32> = of_tree(Tree::Leaf);
let z2 = set_value(42, z);
assert_eq!(z2.focus, Tree::Leaf);
}
#[test]
fn test_to_tree_rebuilds_after_edit() {
let z = of_tree(sample_tree());
let z = go_left(z).expect("left");
let z = set_value(10, z);
let result = to_tree(z);
let expected = Tree::node(
Tree::node(Tree::Leaf, 10, Tree::Leaf),
2,
Tree::node(Tree::Leaf, 3, Tree::Leaf),
);
assert_eq!(result, expected);
}
#[test]
fn test_to_tree_recursive_matches_iter() {
let z = of_tree(sample_tree());
let z = go_right(z).expect("right");
let z = set_value(30, z);
let result = to_tree_recursive(z);
let expected = Tree::node(
Tree::node(Tree::Leaf, 1, Tree::Leaf),
2,
Tree::node(Tree::Leaf, 30, Tree::Leaf),
);
assert_eq!(result, expected);
}
#[test]
fn test_go_left_on_leaf_returns_none() {
let z: Zipper<i32> = of_tree(Tree::Leaf);
assert!(go_left(z).is_none());
}
#[test]
fn test_go_up_at_root_returns_none() {
let z = of_tree(sample_tree());
assert!(go_up(z).is_none());
}
#[test]
fn test_deep_navigation_and_edit() {
// Build a 3-level tree: root=5, left subtree root=3, its left child=1
let tree = Tree::node(
Tree::node(Tree::node(Tree::Leaf, 1, Tree::Leaf), 3, Tree::Leaf),
5,
Tree::Leaf,
);
let z = of_tree(tree);
let z = go_left(z).expect("left");
let z = go_left(z).expect("left-left");
let z = set_value(99, z);
let result = to_tree(z);
let expected = Tree::node(
Tree::node(Tree::node(Tree::Leaf, 99, Tree::Leaf), 3, Tree::Leaf),
5,
Tree::Leaf,
);
assert_eq!(result, expected);
}
}
Deep Comparison
OCaml vs Rust: Tree Zipper
Side-by-Side Code
OCaml
type 'a tree = Leaf | Node of 'a tree * 'a * 'a tree
type 'a crumb = Left of 'a * 'a tree | Right of 'a tree * 'a
type 'a zipper = { focus: 'a tree; trail: 'a crumb list }
let of_tree t = { focus = t; trail = [] }
let go_left z = match z.focus with
| Leaf -> None
| Node (l, v, r) -> Some { focus = l; trail = Left (v, r) :: z.trail }
let go_up z = match z.trail with
| [] -> None
| Left (v, r) :: rest -> Some { focus = Node (z.focus, v, r); trail = rest }
| Right (l, v) :: rest -> Some { focus = Node (l, v, z.focus); trail = rest }
let set_value x z = match z.focus with
| Leaf -> z
| Node (l, _, r) -> { z with focus = Node (l, x, r) }
let rec to_tree z = match go_up z with
| None -> z.focus
| Some z' -> to_tree z'
Rust (idiomatic — iterative to_tree)
#[derive(Debug, Clone, PartialEq)]
pub enum Tree<T> {
Leaf,
Node(Box<Tree<T>>, T, Box<Tree<T>>),
}
#[derive(Debug, Clone)]
pub enum Crumb<T> {
Left(T, Tree<T>),
Right(Tree<T>, T),
}
#[derive(Debug, Clone)]
pub struct Zipper<T> {
pub focus: Tree<T>,
pub trail: Vec<Crumb<T>>,
}
pub fn go_up<T>(mut z: Zipper<T>) -> Option<Zipper<T>> {
match z.trail.pop() {
None => None,
Some(Crumb::Left(v, r)) => Some(Zipper {
focus: Tree::node(z.focus, v, r),
trail: z.trail,
}),
Some(Crumb::Right(l, v)) => Some(Zipper {
focus: Tree::node(l, v, z.focus),
trail: z.trail,
}),
}
}
// Idiomatic: loop + early return avoids the ownership issue
pub fn to_tree<T>(mut z: Zipper<T>) -> Tree<T> {
loop {
if z.trail.is_empty() {
return z.focus;
}
z = go_up(z).expect("trail was non-empty");
}
}
Rust (functional/recursive)
// Mirrors the OCaml tail-recursive style
pub fn to_tree_recursive<T>(z: Zipper<T>) -> Tree<T> {
if z.trail.is_empty() {
return z.focus;
}
to_tree_recursive(go_up(z).expect("trail was non-empty"))
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Tree type | 'a tree = Leaf \| Node of 'a tree * 'a * 'a tree | enum Tree<T> { Leaf, Node(Box<Tree<T>>, T, Box<Tree<T>>) } |
| Crumb type | 'a crumb = Left of 'a * 'a tree \| Right of 'a tree * 'a | enum Crumb<T> { Left(T, Tree<T>), Right(Tree<T>, T) } |
| Zipper type | 'a zipper = { focus: 'a tree; trail: 'a crumb list } | struct Zipper<T> { focus: Tree<T>, trail: Vec<Crumb<T>> } |
go_left | 'a zipper -> 'a zipper option | fn go_left<T>(z: Zipper<T>) -> Option<Zipper<T>> |
set_value | 'a -> 'a zipper -> 'a zipper | fn set_value<T>(x: T, z: Zipper<T>) -> Zipper<T> |
to_tree | 'a zipper -> 'a tree | fn to_tree<T>(z: Zipper<T>) -> Tree<T> |
Key Insights
value implicitly; Rust's move semantics make the same transfer explicit — you
hand over the old Zipper<T> and receive a new one. This prevents accidentally
using a stale zipper after navigation.
to_tree one-liner breaks in Rust.** OCaml can write match go_up z with
None -> z.focus | Some z' -> to_tree z' because the match does not consume z in a way that prevents reading z.focus. In Rust, go_up(z) moves z,
so z.focus is inaccessible in the None arm. The fix is to check the trail
before calling go_up, which is equally clear and efficient.
Vec vs. linked list for the trail.** OCaml's natural crumb list is a linked list (cons-cells, O(1) prepend). Rust's Vec uses push/pop at the end —
also O(1) amortised, but with better cache locality and no per-node allocation.
Box for recursive ADTs.** OCaml trees have a uniform heap layout; Rust enums must have a known size, so recursive variants need Box<Tree<T>> to
break the infinite-size cycle. This is the canonical Rust pattern for recursive
data structures.
{ z with focus = … } vs. struct update.** OCaml's record update syntax ({ z with focus = Node ... }) has a direct Rust analogue in struct update
syntax (Zipper { focus: …, ..z }), but since z is consumed by move we
instead destructure it manually — which is equally concise.
When to Use Each Style
**Use idiomatic Rust (to_tree loop) when:** you want to avoid potential stack
overflow on deep trees, since Rust does not guarantee tail-call optimisation.
**Use recursive Rust (to_tree_recursive) when:** the tree depth is bounded and
you want the code to read as closely as possible to the OCaml original for
educational or porting purposes.
Exercises
navigate_right and navigate_left for the tree zipper, enabling horizontal movement between siblings.map_focused function that applies a transformation to the currently focused node and returns the updated zipper.up, down_left, down_right, insert_left, insert_right, and delete_focus, reconstructing the full tree after a sequence of operations.