Reputation: 362
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
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