Отображение функции на значениях карты в Clojure

139

Я хочу преобразовать одну карту значений в другую карту с теми же ключами, но с функцией, примененной к значениям. Я бы подумал, что в api 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существует ли уже? Я думаю, да (возможно, и с более красивым названием).

Томас
источник

Ответы:

156

Мне твоя reduceверсия нравится . Я считаю это идиоматическим. Вот версия, в любом случае использующая понимание списка.

(defn foo [m f]
  (into {} (for [[k v] m] [k (f v)])))
Брайан Карпер
источник
1
Мне нравится эта версия, потому что она очень короткая и очевидная, если вы понимаете все функции, которые она использует. А если нет, то это повод их изучить!
Runevault
Согласен. Я не знал функции into, но здесь имеет смысл использовать ее.
Thomas
О, человек, которого ты не видел? Вас ждет угощение. Я чертовски злоупотребляю этой функцией при каждой возможности. Такой мощный и полезный.
Runevault
3
Мне нравится, но есть ли причина для порядка аргументов (кроме вопроса)? Я mapпочему-то ожидал [фм] как в .
nha
2
Я настоятельно рекомендую использовать (пустой m), а не {}. Так что это будет особый тип карты.
nickik
97

Вы можете использовать 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 03
Это вошло в Clojure 1.3.0?
AnnanFay
13
библиотека generic.function перемещена в отдельную библиотеку: org.clojure/algo.generic "0.1.0" теперь в примере должно быть написано: (use '[clojure.algo.generic.functor :only (fmap)]) (fmap inc {:a 1 :b 3 :c 5})
Travis Schneeberger
2
Есть идеи, как найти fmapв ClojureScript?
себастибе
38

Вот довольно типичный способ трансформации карты. 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
Меня раздражает то, что я должен предоставить ключи от него, но это не высокая цена. Это определенно выглядит намного лучше, чем мое первоначальное предложение.
Thomas
1
Гарантируется ли нам, что ключи и валы возвращают соответствующие значения в одном и том же порядке? Как для отсортированных карт, так и для хэш-карт?
Роб Лахлан
4
Роб: да, ключи и валы будут использовать один и тот же порядок для всех карт - тот же порядок, что и в последовательности на карте. Поскольку хэш, сортировка и карты массивов неизменяемы, нет никаких шансов на изменение порядка за это время.
Chouser 05
2
Кажется, что это так, но документировано ли это где-нибудь? По крайней мере, в документации для ключей и валов об этом не упоминается. Мне было бы удобнее использовать это, если бы я мог указать на некоторую официальную документацию, которая обещает, что это будет работать.
Jouni K. Seppänen
1
@Jason Да, кажется, они добавили это в документацию в версии 1.6. dev.clojure.org/jira/browse/CLJ-1302
Йоуни К. Сеппянен
23

Взято из Поваренной книги Clojure, есть сокращение-kv:

(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
Если это не ясно: функция anon разрушает ключ и значение до k и 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 создает небольшой список из двух ключей для каждого ключа и его значения. Mapcat запускает эту функцию над последовательностью ключей карты и объединяет все работы в один большой список. "apply hash-map" создает новую карту из этой последовательности. (% M) может выглядеть немного странно, это идиоматический Clojure для применения ключа к карте для поиска связанного значения.

Настоятельно рекомендуется прочитать: Шпаргалка по Clojure .

Карл Смотрич
источник
Я думал о прохождении последовательностей, как вы это делали в своем примере. Мне также нравится, что ваше функциональное имя гораздо больше, чем мое собственное :)
Thomas
1
В Clojure ключевые слова - это функции, которые ищут себя в любой последовательности, переданной им. Вот почему (: ключевое слово a-map) работает. Но использование ключа как функции поиска на карте не работает, если ключ не является ключевым словом. Таким образом, вы можете изменить (% m) выше на (m%), который будет работать независимо от того, какие ключи.
Сиддхартха Редди
Ой! Спасибо за подсказку, Сиддхартха!
Carl Smotricz
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 просто, но код становится намного сложнее, когда вы переходите к сильно вложенному коду, состоящему из различных структур данных. И здесь появляется Spectre .

Я рекомендую посмотреть этот выпуск на Clojure TV, который объясняет мотивацию и детали Spectre .

Мзютер
источник
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"])
Джек Пендлтон
источник