Zipper
A zipper represents an immutable data structure together with a focus, a position inside it. It stores the substructure at the focus, and the path from the focus back to the root, together with everything to either side of that path. Moving the focus one step and editing at the focus take constant time, and the old version remains valid, sharing everything off the path with the new one. Huet introduced the zipper in 1997 for editing trees[1]. The type of a zipper's contexts is the derivative of the structure's type, in the sense of calculus[2].
Overview
An editor for immutable trees has a problem that a mutable one does not: changing a node deep inside means rebuilding every node on the path from it to the root, and finding that node again for the next edit means walking down from the root again. The zipper keeps the path. Walking down from the root, it records at each step which child it took and the parts of the node it did not enter; these records are the context. The focused subtree together with its context is equivalent to the whole tree, and the tree can be rebuilt from them by walking back up.
Seen this way, a zipper is the tree with the pointers on the path to the focus reversed. The Schorr–Waite graph-marking algorithm walks a structure with the same pointer reversal, performed in place[6]; the zipper does it persistently, by allocating new path records instead of overwriting pointers.
Lists
The simplest zipper is for lists. A position between two elements of a list is determined by the elements before it and the elements after it. Storing the elements before it in reverse, nearest first, makes every operation at the position constant-time:
A list zipper used as a text buffer with a cursor.
(* A list zipper: the elements before the focus, nearest first, and theelements from the focus on. Used here as a text buffer with a cursor. *)type 'a zipper = { before : 'a list; after : 'a list }let of_list xs = { before = []; after = xs }let to_list z = List.rev_append z.before z.afterlet left z = match z.before with x :: b -> { before = b; after = x :: z.after } | [] -> zlet right z = match z.after with x :: a -> { before = x :: z.before; after = a } | [] -> zlet insert x z = { z with before = x :: z.before }let backspace z = match z.before with _ :: b -> { z with before = b } | [] -> zlet delete z = match z.after with _ :: a -> { z with after = a } | [] -> zlet rec repeat n f z = if n = 0 then z else repeat (n - 1) f (f z)let show z =let s l = String.of_seq (List.to_seq l) ins (List.rev z.before) ^ "|" ^ s z.after
Running it; | is the cursor.
start |the zipperright x4 the |zipperdelete x6 the |insert "fast " the fast |left x2 the fas|tbackspace the fa|t
Moving the cursor moves one element from one list to the other, and inserting or deleting at the cursor conses onto or removes from the head of one of them. This is the functional counterpart of a gap buffer, the array with a gap at the cursor used by text editors such as Emacs.
Trees
For binary trees, a step of the path records whether the step went left or right, the value at the node it passed through, and the subtree it did not enter. A zipper is the focused subtree and the list of steps from the focus back to the root:
A binary-tree zipper: moving down, rebuilding on the way up, and editing.
type 'a tree = Leaf | Node of 'a tree * 'a * 'a tree(* One step of the path back to the root: which way we went, and what wepassed by, the parent's value and the other subtree. *)type 'a step = WentLeft of 'a * 'a tree | WentRight of 'a tree * 'atype 'a zipper = { focus : 'a tree; path : 'a step list }let of_tree t = { focus = t; path = [] }let down_left z =match z.focus with Node (l, v, r) -> Some { focus = l; path = WentLeft (v, r) :: z.path } | Leaf -> Nonelet down_right z =match z.focus with Node (l, v, r) -> Some { focus = r; path = WentRight (l, v) :: z.path } | Leaf -> Nonelet up z =match z.path with| WentLeft (v, r) :: p -> Some { focus = Node (z.focus, v, r); path = p }| WentRight (l, v) :: p -> Some { focus = Node (l, v, z.focus); path = p }| [] -> Nonelet rec to_tree z = match up z with Some z' -> to_tree z' | None -> z.focuslet modify f z = match z.focus with Node (l, v, r) -> { z with focus = Node (l, f v, r) } | Leaf -> zlet rec show = function| Leaf -> "."| Node (Leaf, v, Leaf) -> string_of_int v| Node (l, v, r) -> "(" ^ show l ^ " " ^ string_of_int v ^ " " ^ show r ^ ")"let ( >>= ) = Option.bind
Running it.
tree ((1 2 3) 4 (5 6 7))focus 5path length 2after edit ((1 2 3) 4 (500 6 7))left subtree shared: trueright subtree shared: falseoriginal unchanged ((1 2 3) 4 (5 6 7))
Going down pushes a step; going up pops one and rebuilds a single node. An edit at depth followed by returning to the root allocates nodes, those on the path. The rest of the tree is shared between the old version and the new one: the physical equality test shows that the untouched left subtree is the same object in both, which is what makes the structure persistent[7]. An editor can keep the previous versions for undo at a cost proportional to the edited paths.
Huet's zipper
Huet's paper treats trees whose nodes have any number of children. A step of the path records the siblings to the left of the focus, nearest first, the path above, and the siblings to the right, so the focus can also move sideways in constant time[1]:
Huet's zipper, with his example of the tree for a * b + c * d.
(* Huet's zipper for trees with any number of children, following his1997 paper. A path records the siblings to the left (nearest first),the path above, and the siblings to the right. *)type tree = Item of string | Section of tree listtype path = Top | Node of tree list * path * tree listtype location = Loc of tree * pathlet go_left (Loc (t, p)) =match p with| Node (l :: left, up, right) -> Loc (l, Node (left, up, t :: right))| _ -> failwith "left of first"let go_right (Loc (t, p)) =match p with| Node (left, up, r :: right) -> Loc (r, Node (t :: left, up, right))| _ -> failwith "right of last"let go_up (Loc (t, p)) =match p with| Node (left, up, right) -> Loc (Section (List.rev_append left (t :: right)), up)| Top -> failwith "up of top"let go_down (Loc (t, p)) =match t with| Section (t1 :: trees) -> Loc (t1, Node ([], p, trees))| _ -> failwith "down of item or empty"let change (Loc (_, p)) t = Loc (t, p)let insert_right (Loc (t, p)) r =match p with Node (left, up, right) -> Loc (t, Node (left, up, r :: right)) | Top -> failwith "insert of top"let rec root (Loc (t, p) as l) = match p with Top -> t | _ -> root (go_up l)let rec show = function| Item s -> s| Section ts -> "[" ^ String.concat " " (List.map show ts) ^ "]"
Running it.
tree [[a * b] + [c * d]]focus dedited [[a * b] + [c * e f]]
The navigation functions are partial: at the top there is no parent, and at the last child nothing to the right. Huet's paper uses them to build a structure editor and discusses scars, a variant that remembers which child was last visited so that going up and down again returns to it[1].
Derivatives
McBride observed that the context type of a zipper can be computed from the structure's type by differentiating it[2]. Read a type as an expression in the element type , with for sum types, for product types, for the unit type and for the empty type. A one-hole context is a value of the type with one occurrence of removed, and removing an follows the rules of differentiation:
The product rule says that the hole in a pair is either in the first component, with the second intact, or in the second, with the first intact. For a triple the derivative is : the hole is in one of three positions, and the other two elements remain. The number of pairs of a value and one of its positions, counted over all values with elements from an -element set, is times the number of contexts, since a pair is a context with an element in the hole:
Symbolic derivatives of polynomial types, with the contexts counted.
(* Polynomial types in one variable x, their derivatives, and a check thatthe derivative counts one-hole contexts. *)type ty = Zero | One | X | Plus of ty * ty | Times of ty * tylet rec d = function| Zero | One -> Zero| X -> One| Plus (a, b) -> Plus (d a, d b)| Times (a, b) -> Plus (Times (d a, b), Times (a, d b))(* The values of a type when x has n elements: E i is an element. *)type v = U | E of int | L of v | R of v | P of v * vlet rec values n = function| Zero -> []| One -> [ U ]| X -> List.init n (fun i -> E i)| Plus (a, b) -> List.map (fun v -> L v) (values n a) @ List.map (fun v -> R v) (values n b)| Times (a, b) -> List.concat_map (fun u -> List.map (fun w -> P (u, w)) (values n b)) (values n a)let rec holes = function U -> 0 | E _ -> 1 | L v | R v -> holes v | P (a, b) -> holes a + holes blet size n t = List.length (values n t)(* The number of values as a polynomial in the size of x: coefficients,constant term first. *)let rec add a b = match (a, b) with [], c | c, [] -> c | x :: a, y :: b -> (x + y) :: add a blet rec mul a b = match a with [] -> [] | x :: a -> add (List.map (( * ) x) b) (0 :: mul a b)let rec poly = function| Zero -> [] | One -> [ 1 ] | X -> [ 0; 1 ]| Plus (a, b) -> add (poly a) (poly b)| Times (a, b) -> mul (poly a) (poly b)let show p =let term i c = match (i, c) with 0, c -> string_of_int c | 1, 1 -> "x" | 1, c -> Printf.sprintf "%dx" c| i, 1 -> Printf.sprintf "x^%d" i | i, c -> Printf.sprintf "%dx^%d" c i inmatch List.filteri (fun _ (_, c) -> c <> 0) (List.mapi (fun i c -> (i, c)) p) with| [] -> "0"| ts -> String.concat " + " (List.map (fun (i, c) -> term i c) ts)
Running it.
triplesF = x^3dF = 3x^2n = 3: holes in all values = 81, n * |dF| = 81lists of length at most 3F = 1 + x + x^2 + x^3dF = 1 + 2x + 3x^2n = 3: holes in all values = 102, n * |dF| = 102binary trees of depth at most 2F = 1 + x + 2x^2 + x^3dF = 1 + 4x + 3x^2n = 3: holes in all values = 120, n * |dF| = 120
Recursive types
Lists satisfy . Differentiating both sides and solving for :
using . A list with a hole is a pair of lists, the elements before the hole and the elements after it: the list zipper. For a recursive type the general rule is a chain rule[3]:
The list is the path of steps down to the node that contains the hole, each step a context for one recursive position, and the last factor is the context of the element within that node. Binary trees have , so and :
A step is one of two alternatives, each with the parent's value and the other subtree, which is the step type above, and the hole sits in a node whose two subtrees are the . A zipper focused on a whole subtree, as in the code, uses the path alone: .
Combinatorial species
The same derivative appears in Joyal's theory of combinatorial species, which treats classes of labelled structures as formal power series[4]. The derivative of a species is the species of structures with one extra point, unlabelled, and the operations of calculus on generating functions correspond to operations on structures[5]. McBride's observation and its generalization to containers by Abbott, Altenkirch, Ghani and McBride carry this over to data types[3]. A related operation, dissection, splits a structure into a part already processed and a part still to be processed, like the two sides of a zipper with different element types, and can be used to derive tail-recursive traversals[11].
Applications
Structure editors were the original application: the focus is the cursor, and every editing command is a local operation at it[1]. The xmonad window manager keeps each workspace's windows in a list zipper, with the focused window at the focus, so that moving focus and swapping windows are local[10]. Zippers are used for navigating file systems and XML documents, for tracking the position in a traversal so that it can be suspended and resumed, and in theorem provers to represent a proof state with a focused goal. Finger trees apply a related idea to sequences, keeping both ends of a balanced tree within constant distance of the root.
Writing a zipper by hand for every data type is repetitive. Generic programming libraries derive them automatically: Hinze and Jeuring gave a generic construction[8], and Adams's "Scrap your zippers" works over any type with a generic traversal[9].
History
Huet published the zipper as a functional pearl in 1997, presenting it as an idea that had been used and rediscovered informally without being published[1]. McBride's note connecting it to differentiation circulated in 2001[2], and Abbott, Altenkirch, Ghani and McBride gave the theory for containers in 2003, published in 2005[3].
see also
- Finger treeA persistent sequence with amortized constant-time access at both ends, and concatenation and splitting in logarithmic time. The ends are kept in buffers of one to four elements, and the middle is a finger tree of 2-3 nodes, one level deeper at each step down the spine.
- Algebraic data typeAn algebraic data type is a type built from sums, where a value is one of several alternatives, and products, where a value has several components at once, possibly recursively. The name comes from counting: the number of values of a sum is the sum of the counts, and of a product their product, so types obey the laws of algebra. Values are taken apart by pattern matching, which the compiler checks for missing cases.
- LensA lens is a first-class pair of a getter and a setter for one part of a structure, with laws that make them agree: you get back what you set, setting what you got changes nothing, and a second set overwrites the first. Lenses compose, so a path through nested immutable records is one value that can read or update the part at its end. They originated in work on the view-update problem, and in Haskell are usually encoded as functions polymorphic in a functor, which makes composition ordinary function composition.
- Red-black treeA red-black tree is a binary search tree whose nodes are coloured red or black so that no red node has a red child and every path from the root to a leaf passes through the same number of black nodes. The two rules keep the height at most 2 log2(n + 1), so search, insertion and deletion take O(log n) time. Red-black trees are an encoding of 2-3-4 trees as binary trees, and Okasaki's functional version reduces insertion to one rebalancing rule with four cases.
- RopeA rope is a binary tree whose leaves are strings and whose internal nodes represent the concatenation of their children, with each node caching the length of its left subtree. Concatenation creates one node instead of copying, and indexing, splitting, insertion and deletion in the middle take time logarithmic in the length for a balanced rope. Ropes are immutable and share structure between versions, which makes them suited to text editors with undo and to programs that build long strings piece by piece.
- Brzozowski derivativeThe Brzozowski derivative of a regular expression r with respect to a character a is a regular expression for the strings w such that a w matches r. It is computed syntactically, by rules much like those of calculus, so a string can be matched by taking one derivative per character and checking whether the result matches the empty string. Up to simple algebraic identities a regular expression has finitely many derivatives, which are the states of a deterministic automaton, and the method extends directly to intersection and complement.
- ComonadA comonad is the categorical dual of a monad: a functor w with extract : w a → a, which reads the value in focus, and extend : (w a → b) → w a → w b, which applies a function that looks at a whole context to every position of that context. Where a monad describes producing a value with an effect, a comonad describes computing a value from a context, such as a cell from its neighbourhood, a value from its history, or a field from a whole structure.
- MonadIn functional programming, a monad is a type constructor m with two operations, return : a -> m a and bind : m a -> (a -> m b) -> m b, satisfying three laws. It lets code with some extra behaviour, such as failure, several results, configuration, state or I/O, be written as a sequence of ordinary steps, with the behaviour defined once in bind. The notion comes from category theory.
- RepminReplace every leaf of a tree by the tree's minimum, in one traversal. The traversal returns the minimum and the rebuilt tree together, and the rebuilt tree's leaves refer to the minimum the same traversal is still computing. It works because the reference is lazy.
- Three-way mergeA three-way merge combines two versions of a document that were derived independently from a common ancestor, the base. Comparing each version with the base shows which side changed each region: a region changed on one side only is taken from that side, a region changed identically on both is taken once, and a region changed differently on both is a conflict left for a person to resolve. The base is what makes the merge possible; with only the two versions, an added line cannot be told from a deleted one. The diff3 algorithm is the standard line-based three-way merge, and it is what version control systems such as Git use to merge files.
further reading
- [1]G. Huet, “The Zipper”, Journal of Functional Programming 7 (1997).
- [2]C. McBride, “The derivative of a regular type is its type of one-hole contexts”, unpublished manuscript (2001).
- [3]M. Abbott, T. Altenkirch, N. Ghani, C. McBride, “∂ for data: differentiating data structures”, Fundamenta Informaticae 65 (2005).
- [4]A. Joyal, “Une théorie combinatoire des séries formelles”, Advances in Mathematics 42 (1981).
- [5]F. Bergeron, G. Labelle, P. Leroux, Combinatorial Species and Tree-like Structures, Cambridge University Press (1998).
- [6]H. Schorr, W. M. Waite, “An efficient machine-independent procedure for garbage collection in various list structures”, Communications of the ACM 10 (1967).
- [7]C. Okasaki, Purely Functional Data Structures, Cambridge University Press (1998).
- [8]R. Hinze, J. Jeuring, “Weaving a web”, Journal of Functional Programming 11 (2001).
- [9]M. D. Adams, “Scrap your zippers: a generic zipper for heterogeneous types”, Workshop on Generic Programming (2010).
- [10]D. Stewart, S. Sjanssen, “XMonad”, Haskell Workshop (2007).
- [11]C. McBride, “Clowns to the left of me, jokers to the right: dissecting data structures”, POPL (2008).
last updated