Clojure: Как применить функцию к подмножеству записей в хэш-карте? - PullRequest
5 голосов
/ 28 октября 2009

Я не в Clojure и пытаюсь выяснить, как это сделать.

Я хочу создать новую хеш-карту, которая для подмножества ключей в хеш-карте применяет функцию к элементам. Каков наилучший способ сделать это?

(let 
   [my-map {:hello "World" :try "This" :foo "bar"}]
   (println (doToMap my-map [:hello :foo] (fn [k] (.toUpperCase k)))

Это должно привести к карте с чем-то вроде

{:hello "WORLD" :try "This" :foo "BAR"}

Ответы [ 3 ]

24 голосов
/ 28 октября 2009
(defn do-to-map [amap keyseq f]
  (reduce #(assoc %1 %2 (f (%1 %2))) amap keyseq))

Разбивка:

Это помогает смотреть на это наизнанку. В Clojure хеш-карты действуют как функции; если вы вызываете их как функцию с ключом в качестве аргумента, возвращается значение, связанное с этим ключом. Таким образом, с учетом одного ключа, текущее значение для этого ключа может быть получено через:

(some-map some-key)

Мы хотим взять старые значения и изменить их на новые, вызвав для них некоторую функцию f. Таким образом, учитывая один ключ, новое значение будет:

(f (some-map some-key))

Мы хотим связать это новое значение с этим ключом в нашей хэш-карте, «заменив» старое значение. Вот что assoc делает:

(assoc some-map some-key (f (some-map some-key)))

(«Заменить» в кавычках, потому что мы не мутируем ни один объект хэш-карты; мы возвращаем новые, неизменные, измененные объекты хэш-карты каждый раз, когда мы вызываем assoc. Это все еще быстро и эффективен в Clojure, потому что хеш-карты являются постоянными и имеют общую структуру, когда вы assoc их.)

Нам нужно многократно assoc добавлять новые значения на нашу карту, по одному ключу за раз. Итак, нам нужна какая-то циклическая конструкция. Нам нужно начать с нашей исходной хэш-карты и одного ключа, а затем «обновить» значение этого ключа. Затем мы берем эту новую хэш-карту и следующий ключ и «обновляем» значение этого следующего ключа. И мы повторяем это для каждого ключа, по одному, и, наконец, возвращаем хэш-карту, которую мы «накопили». Это то, что reduce делает.

  • Первый аргумент reduce - это функция, которая принимает два аргумента: значение «аккумулятора», то есть значение, которое мы постоянно «обновляем» снова и снова; и один аргумент, используемый в одной итерации для накопления.
  • Второй аргумент reduce - это начальное значение, переданное в качестве первого аргумента этому fn.
  • Третий аргумент reduce представляет собой набор аргументов, передаваемых в качестве второго аргумента этому fn, по одному за раз.

Итак:

(reduce fn-to-update-values-in-our-map 
        initial-value-of-our-map 
        collection-of-keys)

fn-to-update-values-in-our-map - это просто оператор assoc сверху, завернутый в анонимную функцию:

(fn [map-so-far some-key] (assoc map-so-far some-key (f (map-so-far some-key))))

Так что подключите его к reduce:

(reduce (fn [map-so-far some-key] (assoc map-so-far some-key (f (map-so-far some-key))))
        amap
        keyseq)

В Clojure есть сокращение для написания анонимных функций: #(...) является анонимным fn, состоящим из одной формы, в которой %1 связан с первым аргументом анонимной функции, %2 - с второе и т. д. Таким образом, наш fn сверху может быть записан эквивалентно как

#(assoc %1 %2 (f (%1 %2)))

Это дает нам:

(reduce #(assoc %1 %2 (f (%1 %2))) amap keyseq)
4 голосов
/ 29 октября 2009
(defn doto-map [m ks f & args]
  (reduce #(apply update-in %1 [%2] f args) m ks))

Пример вызова

user=> (doto-map {:a 1 :b 2 :c 3} [:a :c] + 2)
{:a 3, :b 2, :c 5}

Надеется, что это поможет.

1 голос
/ 28 октября 2009

Кажется, работает следующее:

(defn doto-map [ks f amap]
  (into amap
    (map (fn [[k v]] [k (f v)])
         (filter (fn [[k v]] (ks k)) amap))))

user=> (doto-map #{:hello :foo} (fn [k] (.toUpperCase k)) {:hello "World" :try "This" :foo "bar"})
{:hello "WORLD", :try "This", :foo "BAR"}

Возможно, есть лучший способ сделать это. Возможно, кто-то может придумать хороший однострочник:)

...