code-reviews

Jonas Östlund 2025-05-06T11:36:33.276519Z

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?

Jonas Östlund 2025-05-08T10:31:23.972009Z

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!

👍 2
phronmophobic 2025-05-06T17:25:27.807929Z

> 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

phronmophobic 2025-05-06T17:27:22.819579Z

https://grishaev.me/en/clojure-zippers/ provides a long tutorial.

phronmophobic 2025-05-06T17:30:41.205009Z

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)))))

phronmophobic 2025-05-06T17:31:56.363939Z

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.

Jonas Östlund 2025-05-07T06:28:23.742129Z

Thanks, this seems to be what I was looking for! I will try out the depth-first search using zippers later.

adi 2025-05-19T18:25:42.327629Z

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