В 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))  
Другие вопросы по тегам