🦀 Functional Rust

Example 061: Binary Tree — Size, Membership, Traversal

Difficulty: ⭐⭐ Category: Data Structures Concept: Recursive algebraic data types for binary trees, implementing fundamental operations (size, depth, membership, traversal) via structural recursion. This is the cornerstone pattern for tree-based data structures in functional programming. OCaml → Rust insight: Rust requires `Box<T>` for recursive enum variants because it must know the size of every type at compile time — OCaml's GC handles indirection transparently.
/// 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]);
    }
}

fn main() {
    println!("{:?}", size(&sample_tree()), 5);
    println!("{:?}", size::<i32>(&Leaf), 0);
    println!("{:?}", size(&Tree::node(1, Leaf, Leaf)), 1);
}
type 'a tree =
  | Leaf
  | Node of 'a * 'a tree * 'a tree

let rec size = function
  | Leaf           -> 0
  | Node (_, l, r) -> 1 + size l + size r

let rec depth = function
  | Leaf           -> 0
  | Node (_, l, r) -> 1 + max (depth l) (depth r)

let rec mem x = function
  | Leaf           -> false
  | Node (v, l, r) -> v = x || mem x l || mem x r

(* Linear-time preorder using accumulator *)
let preorder t =
  let rec go acc = function
    | Leaf           -> acc
    | Node (v, l, r) -> v :: go (go acc r) l
  in go [] t

(*      4
       / \
      2   5
     / \
    1   3   *)
let t = Node (4, Node (2, Node (1, Leaf, Leaf), Node (3, Leaf, Leaf)), Node (5, Leaf, Leaf))

let () =
  assert (size t = 5);
  assert (size Leaf = 0);
  assert (depth t = 3);
  assert (mem 3 t = true);
  assert (mem 99 t = false);
  assert (preorder t = [4; 2; 1; 3; 5]);
  print_endline "All assertions passed."

📊 Detailed 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

ConceptOCamlRust
MemoryGC-managed, implicit indirection`Box<T>` for explicit heap allocation
Recursive typesDirect, no annotation neededRequires `Box` to break infinite size
EqualityStructural `=` on any typeRequires `PartialEq` trait bound
Polymorphism`'a` type parameter`<T>` generic with trait bounds
TraversalReturns new list, GC cleans upBorrows 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 reference
  • You must choose: return `Vec<&T>` (borrowing) or `Vec<T>` (cloning/moving). OCaml doesn't force this choice because GC manages lifetimes
  • Helper constructors like `Tree::node()` reduce `Box::new()` noise — a common Rust pattern for recursive types
  • The `#[derive(Debug, Clone, PartialEq)]` line is Rust's way of opting into capabilities that OCaml provides by default
  • Iterative traversal with an explicit stack avoids deep recursion stack overflow — important in Rust where stack size is bounded

Further Reading

  • [The Rust Book — Enums](https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html)
  • [The Rust Book — Box<T>](https://doc.rust-lang.org/book/ch15-01-box.html)
  • [OCaml Trees](https://cs3110.github.io/textbook/chapters/data/trees.html)