What is a Zipper?
A Zipper is a functional data structure used to represent a cursor or pointer focused on a specific subcomponent of a data structure (such as a tree or list). It stores:
- Focus: The subtree currently being inspected or modified.
- Context (Breadcrumbs): The path taken from the root to the focus, allowing you to reconstruct the parents and siblings in time.
Explanation
- In functional programming, data structures are immutable. If you want to modify a leaf node in a large binary tree, you cannot just overwrite its value; you must duplicate and rebuild all the parent nodes up to the root. A Zipper turns the data structure “inside out” around the node of interest (the focus), allowing local traversals and edits to run in time.
Real-World Analogy
- Zip-Line Trolley with Breadcrumbs: Imagine traversing a dense forest canopy using a zip-line harness. The trolley holds you at your current tree branch (the focus). Behind you, a string of markers (breadcrumbs) records every left or right turn you took from the forest entry point. If you want to change a branch or climb back up to the entrance, you only have to look at the immediate string of markers, avoiding search operations.
How It Works
Core Mechanics
- Let’s represent a Binary Tree Zipper.
1. Tree Node Structure
- A normal binary tree node:
Node(value, left, right).
2. Breadcrumb (Context element)
- A breadcrumb tracks a single step up the tree. It stores:
direction: Whether we traversedLeftorRightto reach the current child.value: The value of the parent node.sibling: The sibling subtree that was not traversed (e.g. if we went left, the right child).
3. Zipper Operations
go_left():- Move the focus to the current node’s left child.
- Push a new breadcrumb onto the stack containing
(Direction.LEFT, parent_value, right_sibling).
go_right():- Move the focus to the current node’s right child.
- Push a new breadcrumb onto the stack containing
(Direction.RIGHT, parent_value, left_sibling).
go_up():- Pop a breadcrumb from the stack.
- Reconstruct the parent node using the popped value, sibling, direction, and current focused node.
- Focus on the reconstructed parent.
update(new_value):- Replaces the current focused node with a new node containing
new_value. Takes time.
- Replaces the current focused node with a new node containing
Visual Walkthrough
Tree structure: Root (1) with left (2) and right (3)
1
/ \
2 3
Zipper initialized at Root (1)
- Focus:
Node(1, Node(2), Node(3)) - Breadcrumbs:
[](Empty)
Move Left (go_left())
- We transition focus to
2. The parent value1and sibling3are saved as context: - Focus:
Node(2) - Breadcrumbs:
[LeftStep(parent_val=1, sibling=Node(3))]
Update node value to 99 (update(99))
- Update the local focus without rebuilding the root yet:
- Focus:
Node(99) - Breadcrumbs:
[LeftStep(parent_val=1, sibling=Node(3))]
Move Up (go_up())
- Rebuild the parent node. Since the breadcrumb direction was
Left, the updated node99becomes the parent’s new left child: - Focus:
Node(1, Node(99), Node(3)) - Breadcrumbs:
[]
Time & Space Complexity
| Operation | Standard Immutable Tree (No Zipper) | Binary Tree Zipper |
|---|---|---|
| Move Left/Right | (rebuild path) | |
| Move Up | ||
| Modify focused node | ||
| Reconstruct Root | - | |
| Space Complexity | auxiliary | (for breadcrumbs) |
Implementation
from enum import Enum
class Direction(Enum):
LEFT = 1
RIGHT = 2
class TreeNode:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
def __repr__(self):
return f"Node({self.value}, {self.left}, {self.right})"
class Breadcrumb:
def __init__(self, direction, parent_value, sibling):
self.direction = direction
self.parent_value = parent_value
self.sibling = sibling # The subtree we did not enter
class TreeZipper:
def __init__(self, node, breadcrumbs=None):
self.focus = node
self.breadcrumbs = breadcrumbs if breadcrumbs else []
def go_left(self):
"""Focuses on the left child, saving the right child as context."""
if not self.focus or not self.focus.left:
raise ValueError("Cannot move left: Left child does not exist")
# Save context
step = Breadcrumb(Direction.LEFT, self.focus.value, self.focus.right)
new_breadcrumbs = self.breadcrumbs + [step]
return TreeZipper(self.focus.left, new_breadcrumbs)
def go_right(self):
"""Focuses on the right child, saving the left child as context."""
if not self.focus or not self.focus.right:
raise ValueError("Cannot move right: Right child does not exist")
# Save context
step = Breadcrumb(Direction.RIGHT, self.focus.value, self.focus.left)
new_breadcrumbs = self.breadcrumbs + [step]
return TreeZipper(self.focus.right, new_breadcrumbs)
def go_up(self):
"""Reconstructs the parent node and moves focus up."""
if not self.breadcrumbs:
raise ValueError("Cannot move up: Already at the root")
last_step = self.breadcrumbs[-1]
parent_val = last_step.parent_value
sibling = last_step.sibling
# Rebuild node
if last_step.direction == Direction.LEFT:
parent_node = TreeNode(parent_val, self.focus, sibling)
else:
parent_node = TreeNode(parent_val, sibling, self.focus)
return TreeZipper(parent_node, self.breadcrumbs[:-1])
def update(self, new_value):
"""Replaces the value of the focused node."""
if not self.focus:
new_focus = TreeNode(new_value)
else:
new_focus = TreeNode(new_value, self.focus.left, self.focus.right)
return TreeZipper(new_focus, self.breadcrumbs)
def to_root(self):
"""Ascends all the way to the root and returns the reconstructed tree."""
zipper = self
while zipper.breadcrumbs:
zipper = zipper.go_up()
return zipper.focus
# Example Usage
if __name__ == "__main__":
# Build tree: Node(1, Node(2), Node(3))
root = TreeNode(1, TreeNode(2), TreeNode(3))
zipper = TreeZipper(root)
# Traverse Left to node 2, update to 99, and reconstruct root
zipper = zipper.go_left()
zipper = zipper.update(99)
new_root = zipper.to_root()
print("Updated Tree:", new_root) # Node(1, Node(99, None, None), Node(3, None, None))#include <iostream>
#include <vector>
#include <memory>
#include <stdexcept>
enum class Direction { LEFT, RIGHT };
struct TreeNode {
int value;
std::shared_ptr<TreeNode> left;
std::shared_ptr<TreeNode> right;
TreeNode(int val, std::shared_ptr<TreeNode> l = nullptr, std::shared_ptr<TreeNode> r = nullptr)
: value(val), left(l), right(r) {}
};
struct Breadcrumb {
Direction direction;
int parent_value;
std::shared_ptr<TreeNode> sibling;
Breadcrumb(Direction dir, int parentVal, std::shared_ptr<TreeNode> sib)
: direction(dir), parent_value(parentVal), sibling(sib) {}
};
class TreeZipper {
private:
std::shared_ptr<TreeNode> focus;
std::vector<Breadcrumb> breadcrumbs;
public:
TreeZipper(std::shared_ptr<TreeNode> f, std::vector<Breadcrumb> crumbs = {})
: focus(f), breadcrumbs(crumbs) {}
std::shared_ptr<TreeNode> getFocus() const { return focus; }
TreeZipper goLeft() const {
if (!focus || !focus->left) {
throw std::runtime_error("Cannot move left: Left child does not exist");
}
std::vector<Breadcrumb> newCrumbs = breadcrumbs;
newCrumbs.push_back(Breadcrumb(Direction::LEFT, focus->value, focus->right));
return TreeZipper(focus->left, newCrumbs);
}
TreeZipper goRight() const {
if (!focus || !focus->right) {
throw std::runtime_error("Cannot move right: Right child does not exist");
}
std::vector<Breadcrumb> newCrumbs = breadcrumbs;
newCrumbs.push_back(Breadcrumb(Direction::RIGHT, focus->value, focus->left));
return TreeZipper(focus->right, newCrumbs);
}
TreeZipper goUp() const {
if (breadcrumbs.empty()) {
throw std::runtime_error("Cannot move up: Already at the root");
}
Breadcrumb lastCrumb = breadcrumbs.back();
std::vector<Breadcrumb> newCrumbs(breadcrumbs.begin(), breadcrumbs.end() - 1);
std::shared_ptr<TreeNode> parentNode;
if (lastCrumb.direction == Direction::LEFT) {
parentNode = std::make_shared<TreeNode>(lastCrumb.parent_value, focus, lastCrumb.sibling);
} else {
parentNode = std::make_shared<TreeNode>(lastCrumb.parent_value, lastCrumb.sibling, focus);
}
return TreeZipper(parentNode, newCrumbs);
}
TreeZipper update(int newValue) const {
std::shared_ptr<TreeNode> newFocus;
if (!focus) {
newFocus = std::make_shared<TreeNode>(newValue);
} else {
newFocus = std::make_shared<TreeNode>(newValue, focus->left, focus->right);
}
return TreeZipper(newFocus, breadcrumbs);
}
std::shared_ptr<TreeNode> toRoot() const {
TreeZipper temp = *this;
while (!temp.breadcrumbs.empty()) {
temp = temp.goUp();
}
return temp.focus;
}
};
void printTree(const std::shared_ptr<TreeNode>& node) {
if (!node) return;
std::cout << "Node(" << node->value << ", ";
if (node->left) printTree(node->left);
else std::cout << "null";
std::cout << ", ";
if (node->right) printTree(node->right);
else std::cout << "null";
std::cout << ")";
}
int main() {
auto root = std::make_shared<TreeNode>(1, std::make_shared<TreeNode>(2), std::make_shared<TreeNode>(3));
TreeZipper zipper(root);
zipper = zipper.goLeft().update(99);
auto newRoot = zipper.toRoot();
std::cout << "Reconstructed Tree: ";
printTree(newRoot);
std::cout << "\n"; // Node(1, Node(99, null, null), Node(3, null, null))
return 0;
}
When to Use
✅ Use Zippers When:
- You are implementing tree-navigation features inside a purely functional programming language (like Haskell or Clojure).
- You need to perform localized, high-frequency tree edits (e.g. text/source-code syntax tree parsing, XML/HTML nodes editing, directory structure browsers).
- Rebuilding entire paths from the root down to make minor local adjustments is too expensive.
❌ Do NOT Use Zippers When:
- You are programming in an imperative language (Python, C++, Java) where data structures are naturally mutable. In-place changes are faster.
- You require direct, random access to non-adjacent tree components in parallel.
Variations & Related Concepts
- List Zipper: A simpler variant tracking a focused item in a linear list. Represented as two lists:
(items_before, focus, items_after). Moving left pops fromitems_beforeand pushes toitems_after. Useful for text editor line buffers. - Finger Tree: A functional sequence implementation offering amortized access at both ends, generalization of zippers.
Key Takeaways
- Zippers decouple cursor navigation from immutable data structure paths.
- Local edits and moves (left, right, up) run in time.
- Rebuilding the tree root only occurs once navigation is complete.
- Breadcrumbs represent the “inverse” of the traversed tree path.