061 — Binary Tree (Size, Membership, Traversal)
Tutorial Video
Text description (accessibility)
This video demonstrates the "061 — Binary Tree (Size, Membership, Traversal)" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. This example implements a generic binary tree with the core operations from OCaml's CS3110 course: size, depth, membership, and traversal. Key difference from OCaml: 1. **`Box` for recursion**: Rust requires `Box<Tree<T>>` in Node. OCaml allocates all heap values uniformly — no explicit boxing.
Tutorial
The Problem
This example implements a generic binary tree with the core operations from OCaml's CS3110 course: size, depth, membership, and traversal. Unlike the 99 Problems tree (examples 029-040) which focuses on structural puzzles, this tree is a programming exercise in generics and trait bounds — the foundation for building a binary search tree (BST).
Binary trees are the basis for BSTs (BTreeMap, BTreeSet), heaps (BinaryHeap), Huffman coding, and expression trees in compilers. Understanding the generic Tree<T> with trait bounds (PartialEq for mem, Ord for BST) is prerequisite for implementing any tree-based data structure.
🎯 Learning Outcomes
Tree<T> as a generic recursive enum with Box for heap allocationT: PartialEq as a trait bound for membership testingsize, depth, mem, and in/pre/post-order traversalBox::new at call sitesTree<T> with Box<Tree<T>> subtrees and implement size, depth, and mem operationsT: PartialEq as a trait bound only on mem — leave other operations without bounds when they don't need comparisonCode Example
#![allow(clippy::all)]
/// Binary Tree — Size, Membership, Traversal
///
/// A recursive enum mirrors OCaml's algebraic data type for binary trees.
/// Rust's `enum` is the direct equivalent of OCaml's `type 'a tree = Leaf | Node of ...`.
/// Key difference: Rust requires `Box` for recursive types because it needs to know
/// the size at compile time — OCaml's GC handles this transparently.
/// A generic binary tree. `Box` is needed because recursive types
/// would otherwise have infinite size on the stack.
#[derive(Debug, Clone, PartialEq)]
pub enum Tree<T> {
Leaf,
Node(T, Box<Tree<T>>, Box<Tree<T>>),
}
impl<T> Tree<T> {
/// Helper to construct a node without writing Box::new everywhere.
pub fn node(val: T, left: Tree<T>, right: Tree<T>) -> Self {
Tree::Node(val, Box::new(left), Box::new(right))
}
/// Helper to construct a leaf.
pub fn leaf() -> Self {
Tree::Leaf
}
}
/// Count the number of nodes (recursive, mirrors OCaml's `size`).
pub fn size<T>(tree: &Tree<T>) -> usize {
match tree {
Tree::Leaf => 0,
Tree::Node(_, l, r) => 1 + size(l) + size(r),
}
}
/// Compute the depth (height) of the tree.
pub fn depth<T>(tree: &Tree<T>) -> usize {
match tree {
Tree::Leaf => 0,
Tree::Node(_, l, r) => 1 + depth(l).max(depth(r)),
}
}
/// Check membership — requires `PartialEq` for comparison.
/// In OCaml, structural equality is built in; in Rust we use trait bounds.
pub fn mem<T: PartialEq>(x: &T, tree: &Tree<T>) -> bool {
match tree {
Tree::Leaf => false,
Tree::Node(v, l, r) => v == x || mem(x, l) || mem(x, r),
}
}
/// Preorder traversal using an accumulator (tail-recursive style).
/// Returns owned values — requires `Clone` since we borrow the tree.
pub fn preorder<T: Clone>(tree: &Tree<T>) -> Vec<T> {
fn go<T: Clone>(tree: &Tree<T>, acc: &mut Vec<T>) {
match tree {
Tree::Leaf => {}
Tree::Node(v, l, r) => {
acc.push(v.clone());
go(l, acc);
go(r, acc);
}
}
}
let mut result = Vec::new();
go(tree, &mut result);
result
}
/// Inorder traversal — iterative with explicit stack, zero cloning needed
/// if we only collect references.
pub fn inorder<T>(tree: &Tree<T>) -> Vec<&T> {
let mut result = Vec::new();
let mut stack: Vec<&Tree<T>> = Vec::new();
let mut current = tree;
loop {
match current {
Tree::Node(v, l, _r) => {
stack.push(current);
current = l;
}
Tree::Leaf => {
if let Some(node) = stack.pop() {
if let Tree::Node(v, _, r) = node {
result.push(v);
current = r;
}
} else {
break;
}
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use Tree::*;
fn sample_tree() -> Tree<i32> {
// 4
// / \
// 2 5
// / \
// 1 3
Tree::node(
4,
Tree::node(2, Tree::node(1, Leaf, Leaf), Tree::node(3, Leaf, Leaf)),
Tree::node(5, Leaf, Leaf),
)
}
#[test]
fn test_size() {
assert_eq!(size(&sample_tree()), 5);
assert_eq!(size::<i32>(&Leaf), 0);
assert_eq!(size(&Tree::node(1, Leaf, Leaf)), 1);
}
#[test]
fn test_depth() {
assert_eq!(depth(&sample_tree()), 3);
assert_eq!(depth::<i32>(&Leaf), 0);
assert_eq!(depth(&Tree::node(1, Leaf, Leaf)), 1);
}
#[test]
fn test_mem() {
let t = sample_tree();
assert!(mem(&3, &t));
assert!(mem(&4, &t));
assert!(!mem(&99, &t));
assert!(!mem::<i32>(&1, &Leaf));
}
#[test]
fn test_preorder() {
assert_eq!(preorder(&sample_tree()), vec![4, 2, 1, 3, 5]);
assert_eq!(preorder::<i32>(&Leaf), vec![]);
}
#[test]
fn test_inorder() {
assert_eq!(inorder(&sample_tree()), vec![&1, &2, &3, &4, &5]);
assert_eq!(inorder::<i32>(&Leaf), Vec::<&i32>::new());
}
#[test]
fn test_single_node() {
let t = Tree::node(42, Leaf, Leaf);
assert_eq!(size(&t), 1);
assert_eq!(depth(&t), 1);
assert!(mem(&42, &t));
assert_eq!(preorder(&t), vec![42]);
}
}Key Differences
Box for recursion**: Rust requires Box<Tree<T>> in Node. OCaml allocates all heap values uniformly — no explicit boxing.T: PartialEq for mem. OCaml's structural equality works on all types automatically.Tree::node(v, l, r) hides Box::new. OCaml: Node (v, l, r) is direct.#[derive(Debug, Clone, PartialEq)] on Tree. OCaml's structural equality and printing work automatically.T with trait bounds:** Tree<T> where T: PartialEq for membership. The bound is required only for mem — other operations don't need it. Rust's monomorphization generates separate code for each concrete T.Box<Tree<T>> overhead:** Each heap allocation (one per node) has overhead. For performance-critical trees, arena allocation (typed-arena crate) or Vec-based trees are more efficient.(=) works on any type for structural equality — no trait bound needed. Rust requires explicit PartialEq because the compiler doesn't know if two values of type T can be compared.size vs len:** Trees use size() or count() for node count. Rust's built-in collections use len(). The naming convention helps distinguish data structure operations from collection operations.OCaml Approach
OCaml's CS3110 tree: type 'a tree = Leaf | Node of 'a * 'a tree * 'a tree. Operations: let rec size = function Leaf -> 0 | Node (_, l, r) -> 1 + size l + size r. Membership: let rec mem x = function Leaf -> false | Node (v, l, r) -> v = x || mem x l || mem x r. No boxing needed — OCaml's GC handles heap allocation for recursive types.
Full Source
#![allow(clippy::all)]
/// Binary Tree — Size, Membership, Traversal
///
/// A recursive enum mirrors OCaml's algebraic data type for binary trees.
/// Rust's `enum` is the direct equivalent of OCaml's `type 'a tree = Leaf | Node of ...`.
/// Key difference: Rust requires `Box` for recursive types because it needs to know
/// the size at compile time — OCaml's GC handles this transparently.
/// A generic binary tree. `Box` is needed because recursive types
/// would otherwise have infinite size on the stack.
#[derive(Debug, Clone, PartialEq)]
pub enum Tree<T> {
Leaf,
Node(T, Box<Tree<T>>, Box<Tree<T>>),
}
impl<T> Tree<T> {
/// Helper to construct a node without writing Box::new everywhere.
pub fn node(val: T, left: Tree<T>, right: Tree<T>) -> Self {
Tree::Node(val, Box::new(left), Box::new(right))
}
/// Helper to construct a leaf.
pub fn leaf() -> Self {
Tree::Leaf
}
}
/// Count the number of nodes (recursive, mirrors OCaml's `size`).
pub fn size<T>(tree: &Tree<T>) -> usize {
match tree {
Tree::Leaf => 0,
Tree::Node(_, l, r) => 1 + size(l) + size(r),
}
}
/// Compute the depth (height) of the tree.
pub fn depth<T>(tree: &Tree<T>) -> usize {
match tree {
Tree::Leaf => 0,
Tree::Node(_, l, r) => 1 + depth(l).max(depth(r)),
}
}
/// Check membership — requires `PartialEq` for comparison.
/// In OCaml, structural equality is built in; in Rust we use trait bounds.
pub fn mem<T: PartialEq>(x: &T, tree: &Tree<T>) -> bool {
match tree {
Tree::Leaf => false,
Tree::Node(v, l, r) => v == x || mem(x, l) || mem(x, r),
}
}
/// Preorder traversal using an accumulator (tail-recursive style).
/// Returns owned values — requires `Clone` since we borrow the tree.
pub fn preorder<T: Clone>(tree: &Tree<T>) -> Vec<T> {
fn go<T: Clone>(tree: &Tree<T>, acc: &mut Vec<T>) {
match tree {
Tree::Leaf => {}
Tree::Node(v, l, r) => {
acc.push(v.clone());
go(l, acc);
go(r, acc);
}
}
}
let mut result = Vec::new();
go(tree, &mut result);
result
}
/// Inorder traversal — iterative with explicit stack, zero cloning needed
/// if we only collect references.
pub fn inorder<T>(tree: &Tree<T>) -> Vec<&T> {
let mut result = Vec::new();
let mut stack: Vec<&Tree<T>> = Vec::new();
let mut current = tree;
loop {
match current {
Tree::Node(v, l, _r) => {
stack.push(current);
current = l;
}
Tree::Leaf => {
if let Some(node) = stack.pop() {
if let Tree::Node(v, _, r) = node {
result.push(v);
current = r;
}
} else {
break;
}
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use Tree::*;
fn sample_tree() -> Tree<i32> {
// 4
// / \
// 2 5
// / \
// 1 3
Tree::node(
4,
Tree::node(2, Tree::node(1, Leaf, Leaf), Tree::node(3, Leaf, Leaf)),
Tree::node(5, Leaf, Leaf),
)
}
#[test]
fn test_size() {
assert_eq!(size(&sample_tree()), 5);
assert_eq!(size::<i32>(&Leaf), 0);
assert_eq!(size(&Tree::node(1, Leaf, Leaf)), 1);
}
#[test]
fn test_depth() {
assert_eq!(depth(&sample_tree()), 3);
assert_eq!(depth::<i32>(&Leaf), 0);
assert_eq!(depth(&Tree::node(1, Leaf, Leaf)), 1);
}
#[test]
fn test_mem() {
let t = sample_tree();
assert!(mem(&3, &t));
assert!(mem(&4, &t));
assert!(!mem(&99, &t));
assert!(!mem::<i32>(&1, &Leaf));
}
#[test]
fn test_preorder() {
assert_eq!(preorder(&sample_tree()), vec![4, 2, 1, 3, 5]);
assert_eq!(preorder::<i32>(&Leaf), vec![]);
}
#[test]
fn test_inorder() {
assert_eq!(inorder(&sample_tree()), vec![&1, &2, &3, &4, &5]);
assert_eq!(inorder::<i32>(&Leaf), Vec::<&i32>::new());
}
#[test]
fn test_single_node() {
let t = Tree::node(42, Leaf, Leaf);
assert_eq!(size(&t), 1);
assert_eq!(depth(&t), 1);
assert!(mem(&42, &t));
assert_eq!(preorder(&t), vec![42]);
}
}#[cfg(test)]
mod tests {
use super::*;
use Tree::*;
fn sample_tree() -> Tree<i32> {
// 4
// / \
// 2 5
// / \
// 1 3
Tree::node(
4,
Tree::node(2, Tree::node(1, Leaf, Leaf), Tree::node(3, Leaf, Leaf)),
Tree::node(5, Leaf, Leaf),
)
}
#[test]
fn test_size() {
assert_eq!(size(&sample_tree()), 5);
assert_eq!(size::<i32>(&Leaf), 0);
assert_eq!(size(&Tree::node(1, Leaf, Leaf)), 1);
}
#[test]
fn test_depth() {
assert_eq!(depth(&sample_tree()), 3);
assert_eq!(depth::<i32>(&Leaf), 0);
assert_eq!(depth(&Tree::node(1, Leaf, Leaf)), 1);
}
#[test]
fn test_mem() {
let t = sample_tree();
assert!(mem(&3, &t));
assert!(mem(&4, &t));
assert!(!mem(&99, &t));
assert!(!mem::<i32>(&1, &Leaf));
}
#[test]
fn test_preorder() {
assert_eq!(preorder(&sample_tree()), vec![4, 2, 1, 3, 5]);
assert_eq!(preorder::<i32>(&Leaf), vec![]);
}
#[test]
fn test_inorder() {
assert_eq!(inorder(&sample_tree()), vec![&1, &2, &3, &4, &5]);
assert_eq!(inorder::<i32>(&Leaf), Vec::<&i32>::new());
}
#[test]
fn test_single_node() {
let t = Tree::node(42, Leaf, Leaf);
assert_eq!(size(&t), 1);
assert_eq!(depth(&t), 1);
assert!(mem(&42, &t));
assert_eq!(preorder(&t), vec![42]);
}
}
Deep Comparison
Binary Tree — Size, Membership, Traversal: OCaml vs Rust
The Core Insight
Binary trees are the canonical recursive data structure. Both languages use algebraic types (OCaml's type / Rust's enum), but Rust's ownership model forces you to think about where tree nodes live in memory — a distinction OCaml's garbage collector hides entirely.
OCaml Approach
OCaml's type 'a tree = Leaf | Node of 'a * 'a tree * 'a tree is beautifully concise. The GC handles all allocation and deallocation, so recursive types "just work" without any indirection markers. Pattern matching with function keyword makes structural recursion read almost like a mathematical definition. Polymorphism comes free via 'a type parameters, and structural equality (=) works on any type without extra annotations.
Rust Approach
Rust's enum Tree<T> { Leaf, Node(T, Box<Tree<T>>, Box<Tree<T>>) } requires Box for heap allocation of recursive children — without it, the compiler can't compute the enum's size. This is the key tradeoff: explicit memory layout in exchange for zero-cost abstractions and no GC pauses. Generic functions need trait bounds (PartialEq for comparison, Clone for copying values out of borrowed trees). The &Tree<T> borrow pattern lets traversals share data without cloning.
Side-by-Side
| Concept | OCaml | Rust |
|---|---|---|
| Memory | GC-managed, implicit indirection | Box<T> for explicit heap allocation |
| Recursive types | Direct, no annotation needed | Requires Box to break infinite size |
| Equality | Structural = on any type | Requires PartialEq trait bound |
| Polymorphism | 'a type parameter | <T> generic with trait bounds |
| Traversal | Returns new list, GC cleans up | Borrows with &T or clones with Clone |
| Pattern matching | function keyword sugar | match expression |
What Rust Learners Should Notice
Box<T> is Rust's way of saying "this value lives on the heap" — it's a single-owner smart pointer, not a shared referenceVec<&T> (borrowing) or Vec<T> (cloning/moving). OCaml doesn't force this choice because GC manages lifetimesTree::node() reduce Box::new() noise — a common Rust pattern for recursive types#[derive(Debug, Clone, PartialEq)] line is Rust's way of opting into capabilities that OCaml provides by defaultFurther Reading
Exercises
insert(x: T, tree: Tree<T>) -> Tree<T> and search(x: &T, tree: &Tree<T>) -> bool for a BST (requiring T: Ord). Maintain the BST invariant: left < root < right.level_order(tree: &Tree<T>) -> Vec<Vec<T>> using a queue-based BFS. Compare output with preorder and inorder.to_sorted_vec<T: Ord + Clone>(tree: &Tree<T>) -> Vec<T> by inorder traversal of a BST. Verify it produces a sorted sequence.insert<T: Ord>(tree: Tree<T>, value: T) -> Tree<T> that inserts a value into a binary search tree, maintaining the BST ordering invariant.PartialEq**: Implement PartialEq for Tree<T> where T: PartialEq and write tests verifying t == t (reflexivity), t1 == t2 => t2 == t1 (symmetry), and t1 == t2 && t2 == t3 => t1 == t3 (transitivity).