I know that postwalk is very useful for visiting every element in a nested data structure and potentially transform it. However, sometimes we want to also know the path to the element from the root in order to transform it. In my particular use case, I have an OpenAPI specification and I want to remove all empty schemas inside maps under keys application/json, application/edn, etc. Although I could probably use postwalk here I believe the solution would be clunky. So I first use tree-seq to list all the elements of the data structure together with their paths:
(defn tree-seq-with-paths [data]
(tree-seq (fn [[_path data]]
(or (map? data)
(sequential? data)))
(fn [[path data]]
(if (map? data)
(mapv (fn [[k v]] [(conj path k) v]) data)
(map-indexed (fn [i x] [(conj path i) x]) data)))
[[] data]))
(tree-seq-with-paths {:a [1 2 3]})
;; => ([[] {:a [1 2 3]}] [[:a] [1 2 3]] [[:a 0] 1] [[:a 1] 2] [[:a 2] 3])
Then I use this function to list the paths of all the schemas to remove and remove them from the data structure using update-in with dissoc as the update function:
(defn remove-empty-schema [open-api-spec]
(transduce (keep (fn [[path x]]
(when (and (= {} x)
(spec/valid?
(spec/cat :before (spec/* any?)
:format (spec/and string?
#(str/starts-with? % "application/"))
:schema #{"schema"})
path))
path)))
(completing (fn [dst path]
(update-in dst (butlast path) dissoc (last path))))
open-api-spec
(tree-seq-with-paths open-api-spec)))
Is there a more concise and potentially better way of accomplishing this?I tried using zippers now and the fact that I can do a depth first traversal so easily using z/next and z/end? is really cool.
Here is a full example of some code that I wrote just to test it out, that walks a data structure and increments all numbers that are nested under some :inc key in the data structure. I post it here in case anyone else will find it useful too.
(defn zipper [data]
(z/zipper (fn [[_path data]]
(or (map? data)
(sequential? data)))
(fn [[path data]]
(if (map? data)
(mapv (fn [[k v]] [(conj path k) v]) data)
(map-indexed (fn [i x] [(conj path i) x]) data)))
(fn [[path dst] children]
[path (if (map? dst)
(into {}
(map (fn [[path x]] [(last path) x]))
children)
(into []
(map second)
children))])
[[] data]))
(defn transform-by-path [data edit-fn]
(loop [zip (zipper data)]
(if (z/end? zip)
(-> zip z/root second)
(recur (z/next (or (edit-fn zip) zip))))))
(defn inc-demo [data]
(transform-by-path
data
(fn [zip]
(let [[path x] (z/node zip)]
(when (and (number? x) (some #{:inc} path))
(z/replace zip [path (inc x)]))))))
(inc-demo {:a [1 2 3]
:inc [100 200 300]})
;; => {:a [1 2 3], :inc [101 201 301]}
Thanks again!> However, sometimes we want to also know the path to the element from the root in order to transform it. Zippers are great for this sort of thing, https://clojuredocs.org/clojure.zip/zipper
https://grishaev.me/en/clojure-zippers/ provides a long tutorial.
You can do depth first search with z/next . Something like the following:
(require '[clojure.zip :as z])
(loop [zip (z/zipper branch? children make-node root)]
(if (z/end? zip)
(z/root zip)
(let [zip (if (my-pred (z/node zip))
(my-edit zip)
zip)]
(recur (z/next zip)))))The zipper takes care of the tree walking. You can edit nodes "in-place". You can also check nodes relative to your current location in the tree with z/up, z/down, z/left, z/right, etc.
Thanks, this seems to be what I was looking for! I will try out the depth-first search using zippers later.
Cool... Zippers are somewhat mind-melting until they click, and then they're fun! An abandoned experiment to manage a Clojure teaching workshop repo (to transform the "at-home" heavily commented version to a "in-person" less commented one) https://github.com/adityaathalye/clojure-by-example/blob/master/src/clojure_by_example/fun/workshop_fmt.clj