Zippo: additions to the standard clojure.zip package.
The clojure.zip
package is a masterpiece yet misses some utility
functions. For example, finding locations, bulk updates, lookups, breadth-first
traversing and so on. Zippo, the library I’m introducing in this post,
brings some bits of missing functionality.
Installation
Lein:
[com.github.igrishaev/zippo "0.1.0"]
Deps.edn
{com.github.igrishaev/zippo {:mvn/version "0.1.0"}}
Usage & examples
First, import both Zippo and clojure.zip
:
(ns zippo.core-test
(:require
[clojure.zip :as zip]
[zippo.core :as zippo]))
Declare a zipper:
(def z
(zip/vector-zip [1 [2 3] [[4]]]))
Now check out the following Zippo functions.
A finite seq of locations
The loc-seq
funtion takes a location and returns a lazy seq of locations
untill it reaches the end:
(let [locs (zippo/loc-seq z)]
(mapv zip/node locs))
;; get a vector of notes to reduce the output
[[1 [2 3] [[4]]]
1
[2 3]
2
3
[[4]]
[4]
4]
This is quite useful to traverse a zipper without keeping in mind the ending
condition (zip/end?
).
Finding locations
The loc-find
function looks for the first location that matches a predicate:
(let [loc (zippo/loc-find
z
(fn [loc]
(-> loc zip/node (= 3))))]
(is (= 3 (zip/node loc))))
Above, we found a location which node equals 3.
The loc-find-all
function finds all the locatins that match the predicate:
(let [locs (zippo/loc-find-all
z
(zippo/->loc-pred (every-pred int? even?)))]
(is (= [2 4]
(mapv zip/node locs))))
Since the predicate accepts a location, you can check its children, siblings and so on. For example, check if a location belongs to a special kind of parent.
However, most of the time you’re interested in a value (node) rather than a
location. The ->loc-pred
function converts a node predicate, which accepts a
node, into a location predicate. In the example above, the line
(zippo/->loc-pred (every-pred int? even?))
makes a location predicate which node is an even integer.
Updating a zipper
Zippo offers some functions to update a zipper.
The loc-update
one takes a location predicate, an update function and the rest
arguments. Here is how you douple all the even numbers in a nested vector:
(let [loc
(zippo/loc-update
z
(zippo/->loc-pred (every-pred int? even?))
zip/edit * 2)]
(is (= [1 [4 3] [[8]]]
(zip/root loc))))
For the updating function, one may use zip/append-child
to append a child,
zip/remove
to drop the entire location and so on:
(let [loc
(zippo/loc-update
z
(fn [loc]
(-> loc zip/node (= [2 3])))
zip/append-child
:A)]
(is (= [1 [2 3 :A] [[4]]]
(zip/root loc))))
The node-update
function is similar but acts on nodes. Instead of loc-pred
and loc-fn
, it accepts node-pred
and node-fn
what operate on nodes.
(let [loc
(zippo/node-update
z
int?
inc)]
(is (= [2 [3 4] [[5]]]
(zip/root loc))))
Slicing a zipper by layers
Sometimes, you need to slice a zipper on layers. This is what is better seen on a chart:
+---ROOT---+ ;; layer 1
| |
+-A-+ +-B-+ ;; layer 2
| | | | | |
X Y Z J H K ;; layer 3
- Layer 1 is
[Root]
; - Layer 1 is
[A B]
; - Layer 3 is
[X Y Z J H K]
The loc-layers
function takes a location and builds a lazy seq of layers. The
first layer is the given location, then its children, the children of children
and so on.
(let [layers
(zippo/loc-layers z)]
(is (= '(([1 [2 3] [[4]]])
(1 [2 3] [[4]])
(2 3 [4])
(4))
(for [layer layers]
(for [loc layer]
(zip/node loc))))))
Breadth-first seq of locations
The clojure.zip
package uses depth-first method of traversing a
tree. Let’s number the items:
+-----ROOT[1]----+
| |
+----A[2]---+ +---B[6]--+
| | | | | |
X[3] Y[4] Z[5] J[7] H[8] K[9]
This sometimes may end up with an infinity loop when you generate children on the fly.
The loc-seq-breadth
functions offers the opposite way of traversing a zipper:
+-----ROOT[1]----+
| |
+----A[2]---+ +---B[3]--+
| | | | | |
X[4] Y[5] Z[6] J[7] H[8] K[9]
This is useful to solve some special tasks related to zippers.
Lookups
When working with zippers, you often need such functionality as “go
up/left/right until meet something”. For example, from a given location, go up
until a parent has a special attribute. Zippo offers four functions for that,
namely lookup-up
, lookup-left
, lookup-right
, and lookup-down.
All of
them take a location and a predicate:
(let [loc
(zip/vector-zip [:a [:b [:c [:d]]] :e])
loc-d
(zippo/loc-find loc
(zippo/->loc-pred
(fn [node]
(= node :d))))
loc-b
(zippo/lookup-up loc-d
(zippo/->loc-pred
(fn [node]
(and (vector? node)
(= :b (first node))))))]
(is (= :d (zip/node loc-d)))
(is (= [:b [:c [:d]]] (zip/node loc-b))))
In the example above, first we find the :d
location. From there, we go up
until we meet [:b [:c [:d]]]
. If there is no such a location, the result will
be nil.
Also See
The code from this library was used for Clojure Zippers manual – the complete guide to zippers in Clojure from the very scratch.
© 2022 Ivan Grishaev
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter