Картографування функції на значеннях карти в Clojure


138

Я хочу перетворити одну карту значень на іншу карту тими ж ключами, але з функцією, застосованою до значень. Я б подумав, що для цього було функцію в apju clojure, але я не зміг її знайти.

Ось приклад реалізації того, що я шукаю

(defn map-function-on-map-vals [m f]
  (reduce (fn [altered-map [k v]] (assoc altered-map k (f v))) {} m))
(println (map-function-on-map-vals {:a "test" :b "testing"} #(.toUpperCase %)))
{:b TESTING, :a TEST}

Хтось знає, чи map-function-on-map-valsвже існує? Я б подумав, що так і було (напевно, і з приємнішим ім'ям).

Відповіді:


153

Мені подобається ваша reduceверсія. Я думаю, що це ідіоматично. Ось версія, яка використовує розуміння списку в будь-якому випадку.

(defn foo [m f]
  (into {} (for [[k v] m] [k (f v)])))

1
Мені подобається ця версія, тому що вона дуже коротка і очевидна, якщо ви розумієте всі функції і такі, які вона використовує. І якщо ви цього не зробите - це привід вивчити їх!
Runevault

Я згоден. Я не знав функції, але це має сенс використовувати її тут.
Томас

О чоловік, якого ти не бачив? Ви шукаєте частування. Я зловживаю пеклом із цієї функції, кожен шанс, який я отримаю. Настільки потужний і корисний.
Runevault

3
Мені це подобається, але чи є причина в порядку аргументів (крім питання)? Я очікував [fm], як mapна чомусь.
Nha

2
Я настійно рекомендую використовувати (порожній m), а не {}. Таким чином, це було б особливим типом карти.
nickik

96

Ви можете використовувати clojure.algo.generic.functor/fmap:

user=> (use '[clojure.algo.generic.functor :only (fmap)])
nil
user=> (fmap inc {:a 1 :b 3 :c 5})
{:a 2, :b 4, :c 6}

Я знаю, що ця відповідь трохи пізно дивилася на дати, але я вважаю, що це місце.
edwardsmatt

Це зробило це клоджуром 1.3.0?
AnnanFay

13
lib generic.function перемістився до окремої бібліотеки: org.clojure/algo.generic "0.1.0" у прикладі тепер слід читати: (use '[clojure.algo.generic.functor :only (fmap)]) (fmap inc {:a 1 :b 3 :c 5})
Тревіс Шнебергер

2
Будь-яка ідея, як це знайти fmapв ClojureScript?
sebastibe

37

Ось досить типовий спосіб перетворення карти. zipmap бере список ключів і список значень і "робить правильно", створюючи нову карту Clojure. Ви також можете покласти mapклавіші навколо, щоб змінити їх, або обидва.

(zipmap (keys data) (map #(do-stuff %) (vals data)))

або завершити його у своїй функції:

(defn map-function-on-map-vals [m f]
    (zipmap (keys m) (map f (vals m))))

1
Мене це дратує, що я маю поставити ключі від нього, але платити це не висока ціна. Це, безумовно, виглядає набагато приємніше, ніж моя оригінальна пропозиція.
Томас

1
Ми гарантуємо, що ключі та vals повертають відповідні значення в тому ж порядку? Як для відсортованих карт, так і для хеш-карт?
Роб Лаклан

4
Роб: так, ключі та вали використовуватимуть однаковий порядок для всіх карт - той самий порядок, який використовує послідовність на карті. Оскільки хеш-сортовані та масиви маси незмінні, немає шансів змінити порядок у середній час.
Chouser

2
Це, мабуть, так, але чи це документально десь зафіксовано? Принаймні в документах для ключів і валів це не згадується. Мені було б зручніше використовувати це, якби я міг вказати на якусь офіційну документацію, яка обіцяє, що це буде працювати.
Джоні К. Сеппанен

1
@Jason Так, вони додали це до документації у версії 1.6. dev.clojure.org/jira/browse/CLJ-1302
Jouni K. Seppänen

23

Взяте з кухонної книги Clojure, є скорочення:

(defn map-kv [m f]
  (reduce-kv #(assoc %1 %2 (f %3)) {} m))

8

Ось досить ідіоматичний спосіб зробити це:

(defn map-function-on-map-vals [m f]
        (apply merge
               (map (fn [[k v]] {k (f v)})
                    m)))

Приклад:

user> (map-function-on-map-vals {1 1, 2 2, 3 3} inc))
{3 4, 2 3, 1 2}

2
Якщо не ясно: функція анонім destructures ключа і значення до і V , а потім повертає хеш-карта відображення K в (Fv) .
Сіддхартха Редді

6

map-map, map-map-keysІmap-map-values

Я не знаю жодної існуючої функції в Clojure для цього, але ось така функція, map-map-valuesяку ви можете вільно копіювати. Він поставляється з двома тісно пов'язаними функціями, map-mapі map-map-keys, які також відсутні в стандартній бібліотеці:

(defn map-map
    "Returns a new map with each key-value pair in `m` transformed by `f`. `f` takes the arguments `[key value]` and should return a value castable to a map entry, such as `{transformed-key transformed-value}`."
    [f m]
    (into (empty m) (map #(apply f %) m)) )

(defn map-map-keys [f m]
    (map-map (fn [key value] {(f key) value}) m) )

(defn map-map-values [f m]
    (map-map (fn [key value] {key (f value)}) m) )

Використання

Ви можете зателефонувати map-map-valuesтак:

(map-map-values str {:a 1 :b 2})
;;           => {:a "1", :b "2"}

А інші дві функції, як це:

(map-map-keys str {:a 1 :b 2})
;;         => {":a" 1, ":b" 2}
(map-map (fn [k v] {v k}) {:a 1 :b 2})
;;    => {1 :a, 2 :b}

Альтернативні реалізації

Якщо ви хочете, map-map-keysабо map-map-valuesбез більш загальної map-mapфункції, ви можете використовувати ці реалізації, які не покладаються на map-map:

(defn map-map-keys [f m]
    (into (empty m)
        (for [[key value] m]
            {(f key) value} )))

(defn map-map-values [f m]
    (into (empty m)
        (for [[key value] m]
            {key (f value)} )))

Крім того, ось альтернативна реалізація, map-mapщо базується на clojure.walk/walkзамість into, якщо ви віддаєте перевагу цій фразуванні:

(defn map-map [f m]
    (clojure.walk/walk #(apply f %) identity m) )

Версії Parellel - pmap-mapтощо.

Існують також паралельні версії цих функцій, якщо вони потрібні. Вони просто використовують pmapзамість цього map.

(defn pmap-map [f m]
    (into (empty m) (pmap #(apply f %) m)) )
(defn pmap-map-keys [f m]
    (pmap-map (fn [key value] {(f key) value}) m) )
(defn pmap-map-values [f m]
    (pmap-map (fn [key value] {key (f value)}) m) )

1
Перевірте також функцію карт-ваз призматики. Це швидше за допомогою перехідних процесів github.com/Prismatic/plumbing/blob/…
ClojureMostly

2

Я Clojure n00b, тому цілком можуть бути і більш елегантні рішення. Ось моя:

(def example {:a 1 :b 2 :c 3 :d 4})
(def func #(* % %))

(prn example)

(defn remap [m f]
  (apply hash-map (mapcat #(list % (f (% m))) (keys m))))

(prn (remap example func))

Функція anon робить невеликий 2-й список від кожної клавіші та її f'ed значення. Mapcat виконує цю функцію над послідовністю клавіш карти і об'єднує всі роботи в один великий список. "застосувати хеш-карту" створює нову карту з цієї послідовності. (% M) може виглядати трохи дивно, це ідіоматичний Clojure для застосування ключа до карти для пошуку пов'язаного значення.

Найбільш рекомендується прочитати: Clojure Cheat Sheet .


Я думав про перехід послідовностей, як ви робили у своєму прикладі. Мені також подобається, що ти працюєш набагато більше, ніж моя власна :)
Томас

У Clojure ключові слова - це функції, які шукають себе в будь-якій послідовності, яка їм передається. Ось чому (: а-карта ключового слова) працює. Але використання ключа як функції для пошуку себе на карті не працює, якщо ключ не є ключовим словом. Тому ви можете змінити (% m) вище на (m%), яке буде працювати незалежно від клавіш.
Сіддхартха Редді

На жаль! Дякую за пораду, Сіддхартха!
Карл Смотрич

2
(defn map-vals
  "Map f over every value of m.
   Returns a map with the same keys as m, where each of its values is now the result of applying f to them one by one.
   f is a function of one arg, which will be called which each value of m, and should return the new value.
   Faster then map-vals-transient on small maps (8 elements and under)"
  [f m]
  (reduce-kv (fn [m k v]
               (assoc m k (f v)))
             {} m))

(defn map-vals-transient
  "Map f over every value of m.
   Returns a map with the same keys as m, where each of its values is now the result of applying f to them one by one.
   f is a function of one arg, which will be called which each value of m, and should return the new value.
   Faster then map-vals on big maps (9 elements or more)"
  [f m]
  (persistent! (reduce-kv (fn [m k v]
                            (assoc! m k (f v)))
                          (transient {}) m)))

1

Мені подобається ваша reduceверсія. З дуже невеликою різницею, він також може зберігати тип структур записів:

(defn map-function-on-map-vals [m f]
  (reduce (fn [altered-map [k v]] (assoc altered-map k (f v))) m m))

Його {}замінили m. З цією зміною записи залишаються записами:

(defrecord Person [firstname lastname])

(def p (map->Person {}))
(class p) '=> Person

(class (map-function-on-map-vals p
  (fn [v] (str v)))) '=> Person

Починаючи з цього {}, запис втрачає свою записаність , яку ви хочете зберегти, якщо бажаєте можливості запису (наприклад, компактне представлення пам'яті).


0

Я задаюся питанням , чому ніхто не згадав привид бібліотеки ще. Це було написано, щоб зробити такий вид перетворення легким для кодування (і, що ще важливіше, написаний код легко зрозуміти), при цьому він є дуже ефективним:

(require '[com.rpl.specter :as specter])

(defn map-vals [m f]
  (specter/transform
   [specter/ALL specter/LAST]
   f m))

(map-vals {:a "test" :b "testing"}
          #(.toUpperCase %))

Написати таку функцію в чистому Clojure просто, але код стає набагато складніше, коли ви переходите до високо вкладеного коду, що складається з різних структур даних. І ось тут приходить привид .

Рекомендую переглянути цей епізод на Clojure TV, який пояснює мотивацію та деталі привидів .


0

Clojure 1.7 ( вийшов 30 червня 2015 р.) Забезпечує елегантне рішення для цього update:

(defn map-function-on-map-vals [m f]
    (map #(update % 1 f) m))

(map-function-on-map-vals {:a "test" :b "testing"} #(.toUpperCase %))
;; => ([:a "TEST"] [:b "TESTING"])
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.