В Clojure, Как правильно обновить вложенную карту?
Я только начал изучать Clojure после многолетнего опыта работы с Java (и PHP/JavaScript). Что за проблема:-)
Как обновить карту значений идиоматически? Когда я использую map
Функция на карте не возвращает карту, она возвращает последовательность.
Я работаю над небольшим приложением, в котором у меня есть список задач. Что я хотел бы сделать, это изменить некоторые значения в некоторых отдельных задач, а затем обновить список исходных задач. Вот задачи, с которыми я тестирую:
(defrecord Task [key name duration])
(def tasks
(atom
{
"t1" (->Task "t1" "Task 1" 10)
"t2" (->Task "t2" "Task 2" 20)
"t3" (->Task "t3" "Task 3" 30)
}
))
Я поместил задачи в хэш-карту, используя строковый ключ, чтобы он имел быстрый прямой доступ к любой задаче на карте. Каждая задача также содержит ключ, поэтому я знаю, что это за ключ, когда я передаю отдельные задачи другим функциям.
Для обновления продолжительности я использую map
а также update-in
перебирать и выборочно обновлять продолжительность каждой задачи и возвращать измененные задачи.
Вот функция:
(defn update-task-durations
"Update the duration of each task and return the updated tasks"
[tasks]
; 1) Why do I have to convert the result of the map function,
; from a sequence then back to a map?
(into {}
(map
(fn [task]
(println task) ; debug
(update-in
task
; 2) Why do I have to use vector index '1' here
; to get the value of the map entry?
[1 :duration]
(fn [duration]
(if (< duration 20)
(+ duration 1)
(+ duration 2)
)
)
)
) tasks))
)
Я печатаю значения до / после этого:
(println "ORIGINAL tasks:")
(println @tasks)
(swap! tasks update-task-durations)
(println "\nUPDATED tasks:")
(println @tasks)
1) Основная проблема, с которой я сталкиваюсь, заключается в том, что map
Функция возвращает последовательность, а не карту, поэтому мне нужно снова преобразовать последовательность обратно в карту, используя into {}
что мне кажется ненужным и неэффективным.
Есть лучший способ сделать это? Должен ли я использовать функцию, отличную от map
?
Могу ли я упорядочить свои структуры данных лучше, но при этом быть эффективным для прямого доступа к отдельным задачам?
Можно ли преобразовать (потенциально очень большую) последовательность в карту, используя into {}
?
2) Кроме того, внутри моего параметра функции, который я передаю map
функция, каждая задача дается мне, map
как вектор вида [key value]
когда я ожидал запись карты, поэтому, чтобы получить значение из записи карты, я должен передать следующие ключи моему update-in
[1 :duration]
Это кажется немного уродливым, есть ли лучший / более ясный способ получить доступ к записи карты, а не использовать индекс 1 вектора?
2 ответа
Популярный способ решить эту проблему - это zipmap
:
(defn map-vals
"Returns the map with f applied to each item."
[f m]
(zipmap (keys m)
(map f (vals m))))
(defn update-task-durations
[tasks]
(let [update-duration (fn [duration]
(if (< duration 20)
(+ 1 duration)
(+ 2 duration)))]
(->> tasks
(map-vals #(update % :duration update-duration)))))
(swap! tasks update-task-durations)
Для Clojure < 1.7 используйте (update-in % [:duration] ...
вместо.
В качестве альтернативы, вы также можете использовать деструктуризацию, чтобы упростить ваше текущее решение без определения служебной функции:
(->> tasks
(map (fn [[k task]]
[k (update task :duration update-duration)]))
(into {})
Зачем?
map
имеет дело только с последовательностями. Если вы в подписи типа, это означает, что map
всегда имеет один и тот же тип (map :: (a -> b) -> [a] -> [b]
), но это также означает, что все, что вы получите map
это последовательность чего-то.
map
звонки seq
на свой параметр коллекции, прежде чем делать что-либо, и seq
-ing карта дает вам последовательность пар ключ-вал.
Не беспокойтесь об эффективности здесь. into
быстро и это довольно идиоматично.
Просто получите больше альтернатив: вместо map
Вы можете использовать for
(into {}
(for [[key value] your-map]
[key (do-stuff value)]))
Более быстрый способ reduce-kv
(reduce-kv
(fn [new-map key value]
(assoc new-map key (do-stuff value)))
{}
your-map))
Конечно, вы также можете использовать простой reduce
(reduce (fn [m key]
(update m key do-stuff))
your-map
(keys your-map))