I'm using specter to transform nested data structures in Clojure, but I haven't got the hang of it yet. In particular, I'm trying to create a transformation that will find an item - at any depth - that matches a predicate, and replace it will several items.
[:top
[:arbitrary 1 2
[:nesting
2
3
[:needle] ; <-- the thing to find
]]]
-->
[:top
[:arbitrary 1 2
[:nesting
2
3
[:n1] [:n2] [:n3] ; <-- 3 items inserted in the place of 1
]]]
What I can't figure out is how to splice the replacement items into the parent vector, i.e., how to replace one item with three items, and not with one item containing three children.
I don't know how to do this using Specter, but here's a function to do it with clojure.zip:
(defn splice-replace [zipper smap]
(loop [loc zipper]
(if (z/end? loc)
(z/root loc)
(recur
(z/next
(if-let [sub (smap (z/node loc))]
(reduce (comp z/right z/insert-right)
(z/replace loc (first sub))
(rest sub))
loc))))))
You can call it with a zipper of your data structure and a map from values you want to replace to a sequence of their replacement values to be spliced into their position:
(def zipper
(z/vector-zip [:top
[:arbitrary 1 2
[:nesting 2 3 [:needle]]]]))
(splice-replace zipper {[:needle] [[:n1] [:n2] [:n3]]})
=> [:top [:arbitrary 1 2 [:nesting 2 3 [:n1] [:n2] [:n3]]]]
(splice-replace zipper {[:nesting 2 3 [:needle]] (range 3 10)})
=> [:top [:arbitrary 1 2 3 4 5 6 7 8 9]]
(defn replace-needle [input replacement]
(let [needle-parent? #(= % [:needle])
NEEDLE-PARENT (recursive-path
[] p (cond-path
#(and (vector? %) (some needle-parent? %)) [(continue-then-stay [ALL p])]
vector? [ALL p]))
inject-replacement (fn inject [x] (vec (mapcat #(if (needle-parent? %) replacement [%]) x)))]
(transform [NEEDLE-PARENT] inject-replacement input)))
(let [input [:top
[:arbitrary 1 2
[:nesting 2 3 [:needle]]]]
replacement [[:n1] [:n2] [:n3]]]
(replace-needle input replacement))
I thought it should be possible to find a vector that contains [:needle]
, and then the index of the [:needle]
, and then use srange
to splice the new elements into the parent at that index, but I didn't find a way to do it using Specter.
Here's that same idea expressed using clojure.walk
:
(require '[clojure.walk :refer [postwalk]])
(postwalk (fn [node]
(if (and (vector? node)
(some (partial = [:needle]) node))
(let [idx (.indexOf node [:needle])]
(vec (concat (take idx node)
[[:n1] [:n2] [:n3]]
(drop (inc idx) node))))
node))
data)
;; => [:top [:arbitrary 1 2 [:nesting 2 3 [:n1] [:n2] [:n3]]]]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With