Предварительная бесконечная рекурсия Clojure для атомарных запросов

Мне нужно динамически изменять данные этой структуры:

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

к следующему:

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

Поскольку я обрабатываю несколько разных запросов, я могу быть уверен, что соединение будет четвертым элементом в векторе. Но мне нужно заменить каждый экземпляр :list/items на (default :list/items []).

Единственный известный мне способ сделать это — использовать clojure.walk/prewalk. Однако это приводит к бесконечной рекурсии:

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

Как только обход находит :list/items и заменяет его на '(default :list/items []), он затем находит :list/items в замененном значении и заменяет его. И так далее.

Я могу использовать атом, чтобы убедиться, что значение заменяется только один раз, но это похоже на мошенничество.

Любые другие подходы?


person egracer    schedule 13.05.2016    source источник


Ответы (1)


в этом случае вам, вероятно, придется использовать 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 не углубляется в содержимое, даже если лист был заменен новой коллекцией:

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]]

кстати, есть хорошие функции prewalk-replace/postwalk-replace для вашего конкретного случая:

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 []) []}]}]

обновление после комментариев: некоторый (синтетический) пример большего контроля над заменой. Допустим, вы хотите заменить определенные элементы в произвольном наборе вложенных векторов, но заменить элемент только один раз (в первый раз, когда вы его видите), а остальные оставить без изменений:

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]]

(здесь вы просто удаляете замененные элементы из карты кандидатов на замену (rep) и передаете ее дальше с рекурсией, пока она не станет пустой)

person leetwinski    schedule 13.05.2016
comment
О, хорошо, postwalk-replace идеален. Из любопытства предположим, что мне нужно было использовать prewalk, чтобы каким-то другим образом манипулировать запросом. Нужно ли мне использовать атом, чтобы отслеживать замену? Или есть какой-то другой способ? - person egracer; 13.05.2016
comment
какой именно путь вы имеете в виду? не могу сейчас об этом думать) - person leetwinski; 13.05.2016
comment
но я бы предложил вам взглянуть на zippers для этого. Если вам нужен более детальный контроль над заменой и итерацией по дереву. Например, это позволит вам заменить какой-то элемент, а затем пропустить этот элемент, или отслеживать его в каком-то накопителе (так как молнии прекрасно работают с loop/recur - person leetwinski; 13.05.2016
comment
Так бы и получилось, выглядит жестко, но весело. Спасибо! - person egracer; 13.05.2016