Odwzorowanie funkcji na wartościach mapy w Clojure


138

Chcę przekształcić jedną mapę wartości na inną mapę z tymi samymi kluczami, ale z funkcją zastosowaną do wartości. Wydaje mi się, że w interfejsie API clojure istnieje funkcja umożliwiająca wykonanie tej czynności, ale nie mogłem jej znaleźć.

Oto przykładowa implementacja tego, czego szukam

(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}

Czy ktoś wie, czy map-function-on-map-valsjuż istnieje? Myślę, że tak (prawdopodobnie z ładniejszą nazwą).

Odpowiedzi:


153

Podoba mi się twoja reducewersja. Myślę, że to idiomatyczne. Oto wersja, która i tak używa funkcji rozumienia list.

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

1
Podoba mi się ta wersja, ponieważ jest bardzo krótka i oczywista, jeśli rozumiesz wszystkie funkcje i takie, z jakich korzysta. A jeśli nie, to wymówka, aby się ich nauczyć!
Runevault

Zgadzam się. Nie znałem funkcji into, ale użycie jej tutaj ma sens.
Thomas

Człowieku, którego nie widziałeś? Czeka cię przyjemność. Nadużywam tej funkcji przy każdej okazji. Tak potężne i przydatne.
Runevault

3
Podoba mi się, ale czy jest jakiś powód dla kolejności argumentów (poza pytaniem)? Z mapjakiegoś powodu spodziewałem się [fm] .
nha

2
Zdecydowanie zalecam użycie (puste m) zamiast {}. Więc nadal byłby to szczególny rodzaj mapy.
nickik

96

Możesz użyć 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}

Wiem, że ta odpowiedź była trochę spóźniona, patrząc na daty, ale myślę, że jest trafna.
edwardsmatt

Czy to dotarło do clojure 1.3.0?
AnnanFay

13
biblioteka generic.function została przeniesiona do osobnej biblioteki: org.clojure/algo.generic "0.1.0" przykład powinien teraz brzmieć: (use '[clojure.algo.generic.functor :only (fmap)]) (fmap inc {:a 1 :b 3 :c 5})
Travis Schneeberger

2
Masz jakiś pomysł, jak znaleźć fmapw ClojureScript?
sebastibe

37

Oto dość typowy sposób przekształcania mapy. zipmap pobiera listę kluczy i listę wartości i „robi właściwą rzecz” tworząc nową mapę Clojure. Możesz również umieścić mapwokół klawiszy, aby je zmienić, lub oba.

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

lub aby zawrzeć to w swojej funkcji:

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

1
Irytuje mnie, że muszę do niego dostarczyć klucze, ale to nie jest wysoka cena do zapłacenia. Zdecydowanie wygląda o wiele ładniej niż moja oryginalna sugestia.
Thomas

1
Czy gwarantujemy, że klucze i wartości zwracają odpowiednie wartości w tej samej kolejności? W przypadku map posortowanych i map skrótów?
Rob Lachlan

4
Rob: tak, klucze i wartości będą miały tę samą kolejność na wszystkich mapach - taką samą kolejność, jakiej używa sekwencja na mapie. Ponieważ mapy hash, sortowane i tablicowe są niezmienne, nie ma szans na zmianę kolejności w międzyczasie.
Chouser

2
Wydaje się, że tak jest, ale czy jest to gdzieś udokumentowane? Przynajmniej w dokumentach kluczy i wartości nie ma o tym wzmianki. Byłbym wygodniejszy, gdybym mógł wskazać jakąś oficjalną dokumentację, która obiecuje, że zadziała.
Jouni K. Seppänen

1
@Jason Tak, chyba dodali to do dokumentacji w wersji 1.6. dev.clojure.org/jira/browse/CLJ-1302
Jouni K. Seppänen

23

Zaczerpnięte z książki kucharskiej Clojure, jest Redukcja kv:

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

8

Oto dość idiomatyczny sposób, aby to zrobić:

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

Przykład:

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

2
Jeśli nie jest jasne: funkcja anon niszczy klucz i wartość na k i v, a następnie zwraca mapowanie skrótu k do (fv) .
Siddhartha Reddy

6

map-map, map-map-keysimap-map-values

Nie znam żadnej istniejącej funkcji w Clojure do tego, ale oto implementacja tej funkcji, ponieważ map-map-valuesmożesz ją kopiować. Zawiera dwie ściśle powiązane funkcje map-mapi map-map-keys, których również brakuje w standardowej bibliotece:

(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) )

Stosowanie

Możesz zadzwonić w map-map-valuesten sposób:

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

A pozostałe dwie funkcje, takie jak ta:

(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}

Alternatywne implementacje

Jeśli chcesz map-map-keyslub map-map-valuesbez bardziej ogólnej map-mapfunkcji, możesz użyć tych implementacji, które nie polegają na 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)} )))

Poniżej znajduje się alternatywna implementacja, map-mapktóra jest oparta na clojure.walk/walkzamiast into, jeśli wolisz to sformułowanie:

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

Wersje Parellel - pmap-mapitp.

Istnieją również równoległe wersje tych funkcji, jeśli ich potrzebujesz. Po prostu używają pmapzamiast 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
Sprawdź także funkcję mapowania pryzmatów. Jest szybszy przy użyciu transjentów github.com/Prismatic/plumbing/blob/ ...
ClojureMostly

2

Jestem Clojure n00b, więc mogą być znacznie bardziej eleganckie rozwiązania. To moje:

(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))

Funkcja anon tworzy małą listę 2 z każdego klucza i jego wartości f'ed. Mapcat uruchamia tę funkcję na sekwencji kluczy mapy i łączy całość prac w jedną dużą listę. „Apply hash-map” tworzy nową mapę z tej sekwencji. (% M) może wyglądać trochę dziwnie, jest to idiomatyczne Clojure do stosowania klucza do mapy w celu wyszukania powiązanej wartości.

Najbardziej polecana lektura: Ściągawka Clojure .


Myślałem o przejściu przez sekwencje, tak jak zrobiłeś to na swoim przykładzie. Również nazwa twojej funkcji podoba mi się znacznie bardziej niż moja własna :)
Thomas

W Clojure słowa kluczowe to funkcje, które wyszukują same siebie w dowolnej kolejności. Dlatego (: słowo kluczowe a-mapa) działa. Ale użycie klucza jako funkcji wyszukiwania samego siebie na mapie nie działa, jeśli klucz nie jest słowem kluczowym. Więc możesz chcieć zmienić (% m) powyżej na (m%), co będzie działać bez względu na to, jakie są klucze.
Siddhartha Reddy

Ups! Dzięki za wskazówkę, Siddhartha!
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

Podoba mi się twoja reducewersja. Z bardzo niewielkimi odchyleniami może również zachowywać typy struktur rekordów:

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

{}Został zastąpiony m. Po tej zmianie rekordy pozostają zapisami:

(defrecord Person [firstname lastname])

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

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

Rozpoczynając od {}, rekord traci swoją nagrywalność , którą można chcieć zachować, jeśli pożądane są możliwości rekordu (na przykład reprezentacja pamięci kompaktowej).


0

Zastanawiam się, dlaczego nikt jeszcze nie wspomniał o bibliotece widm . Został napisany, aby uczynić ten rodzaj transformacji łatwym do zakodowania (a co ważniejsze, napisany kod był łatwy do zrozumienia), a jednocześnie jest bardzo wydajny:

(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 %))

Pisanie takiej funkcji w czystym Clojure jest proste, ale kod staje się znacznie trudniejszy, gdy przejdziesz do wysoce zagnieżdżonego kodu złożonego z różnych struktur danych. I tu pojawia się widmo .

Polecam obejrzenie tego odcinka w Clojure TV, który wyjaśnia motywację i szczegóły widma .


0

Clojure 1.7 ( wydany 30 czerwca 2015) zapewnia eleganckie rozwiązanie z 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"])
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.