Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure - using specter to transform a nested data structure, replacing one node with several

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.

like image 658
TomSW Avatar asked Mar 14 '19 22:03

TomSW


3 Answers

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]]
like image 100
Taylor Wood Avatar answered Nov 09 '22 08:11

Taylor Wood


(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))
like image 3
akond Avatar answered Nov 09 '22 06:11

akond


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]]]]
like image 1
Erwin Rooijakkers Avatar answered Nov 09 '22 07:11

Erwin Rooijakkers