clojurepattern-matchingzipper

visit clojure.zip tree by path pattern


Say I have a tree where I want to visit - and that should include the possibility to modify the visited items - all items that match the path

(def visit-path [:b :all :x :all])

where I use :all as a wildcard to match all child nodes. In the following example tree,

(def my-tree
  {:a "a"
   :b
   {:b-1
    {:x
     {:b-1-1 "b11"
      :b-1-2 "b12"}}
    :b-2
    {:x
     {:b-2-1 "b21"}}}})

that would be items

Is there an elegant way to do this using clojure core?


FYI, I did solve this by creating my own pattern-visitor

(defn visit-zipper-pattern
  [loc pattern f]

but although this function is generically usable, it is quite complex, combing both stack-consuming recursion and tail-call recursion. So when calling that method like

(visit-zipper-pattern (map-zipper my-tree) visit-path
  (fn [[k v]] [k (str "mod-" v)]))

using map-zipper from https://stackoverflow.com/a/15020649/709537, it transforms the tree to

{:a "a"
 :b {:b-1
     {:x
      {:b-1-1 "mod-b11"
       :b-1-2 "mod-b12"}}
     :b-2
     {:x
      {:b-2-1 "mod-b21"}}}}

Solution

  • The following will work - note that 1) it may allocate unneeded objects when handling:all keys and 2) you need to decide how to handle edge cases like :all on non-map leaves.

    (defn traverse [[k & rest-ks :as pattern] f tree]
      (if (empty? pattern)
        (f tree)
        (if (= k :all)
          (reduce #(assoc %1 %2 (traverse rest-ks f (get tree %2)))
                  tree (keys tree))
          (cond-> tree (contains? tree k)
                  (assoc k (traverse rest-ks f (get tree k)))))))
    

    For a more efficient solution, it's probably better to use https://github.com/nathanmarz/specter as recommended above.