egracer
egracer

Reputation: 362

Clojure prewalk infinite recursion for datomic queries

I need to dynamically modify data of this structure:

[:db/id
 :list/title
 :list/type
 {:list/items [... lots of nested data ...]}]

to the following:

[:db/id
 :list/title
 :list/type
 {(default :list/items []) [... lots of nested data ...]}]

Since I'm handling several different queries, I can be sure that the join will be the fourth item in the vector. But I need to replace every instance of :list/items with (default :list/items []).

The only way that I know to do this is by using clojure.walk/prewalk. However, it leads to infinite recursion:

(clojure.walk/prewalk #(if (= :list/items %)
                        '(default :list/items [])
                        %) 
                      query)

Once the walk finds :list/items and replaces it with '(default :list/items []), it then finds the :list/items in the replaced value, and replaces that. And so on and so forth.

I can use an atom to make sure the value is only replaced once, but that feels like cheating.

Any other approaches?

Upvotes: 0

Views: 146

Answers (1)

leetwinski
leetwinski

Reputation: 17849

in this case you probably have to use postwalk:

user> 
(def query [:db/id
            :list/title
            :list/type
            {:list/items [:db/id
                          :list/title
                          :list/type
                          {:list/items []}]}])
#'user/query

user> (clojure.walk/postwalk #(if (= :list/items %)
                               '(default :list/items [])
                               %) 
                             query)
[:db/id :list/title :list/type 
 {(default :list/items []) [:db/id :list/title :list/type {(default :list/items []) []}]}]

postwalk doesn't go deeper to the contents even if the leaf has been replaced by a new collection:

user> (clojure.walk/prewalk #(do (println %)
                                 (if (= % 1) [10] %))
                            [[1 2 3 [1 2]] [1 2]])
[[1 2 3 [1 2]] [1 2]]
[1 2 3 [1 2]]
1
10 ;; goes deeper
2
3
[1 2]
1
10 ;; and here
2
[1 2]
1
10 ;; and here
2
[[[10] 2 3 [[10] 2]] [[10] 2]]

user> (clojure.walk/postwalk #(do (println %)
                                  (if (= % 1) [10] %))
                             [[1 2 3 [1 2]] [1 2]])
1
2
3
1
2
[[10] 2]
[[10] 2 3 [[10] 2]]
1
2
[[10] 2]
[[[10] 2 3 [[10] 2]] [[10] 2]]
[[[10] 2 3 [[10] 2]] [[10] 2]]

by the way, there is a nice functions prewalk-replace/postwalk-replace for your exact case:

user> (clojure.walk/postwalk-replace 
        {:list/items '(default :list/items [])} query)

[:db/id :list/title :list/type 
 {(default :list/items []) [:db/id :list/title :list/type {(default :list/items []) []}]}]

update, after comments: some (synthetic) example of more control over the replacement. Let's say you want to replace specific items in some arbitrary collection of nested vectors, but to replace the item only once (first time you see it), and leave the rest unchanged:

user> (require '[clojure.zip :as z])

user> 
(defn replace-once [rep coll]
  (loop [curr (z/vector-zip coll) rep rep]
    (if (empty? rep) (z/root curr)
        (let [n (z/node curr) r (rep n)]
          (cond (z/end? curr) (z/root curr)
                r (recur (z/replace curr r) (dissoc rep n))
                :else (recur (z/next curr) rep))))))
#'user/replace-once

user> (replace-once {1 100 2 200} [[4 3 2] [10 1 2] 1 2 [5 3 2]])
[[4 3 200] [10 100 2] 1 2 [5 3 2]]

(here you just remove replaced items from replacement candidates map (rep), and pass it further with the recursion, until it's empty)

Upvotes: 1

Related Questions