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.