Mappatura di una funzione sui valori di una mappa in Clojure


138

Voglio trasformare una mappa di valori in un'altra mappa con le stesse chiavi ma con una funzione applicata ai valori. Penserei che ci fosse una funzione per farlo nell'api del clojure, ma non sono stato in grado di trovarlo.

Ecco un esempio di implementazione di ciò che sto cercando

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

Qualcuno sa se map-function-on-map-valsesiste già? Penserei di sì (probabilmente anche con un nome più carino).

Risposte:


153

Mi piace la tua reduceversione bene. Penso che sia idiomatico. Ecco una versione che utilizza comunque la comprensione dell'elenco.

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

1
Mi piace questa versione perché è super breve e ovvia se capisci tutte le funzioni e tale che utilizza. E se non lo fai è una scusa per impararli!
Runevault,

Sono d'accordo. Non conoscevo la funzione, ma ha perfettamente senso usarlo qui.
Thomas,

Oh amico in cui non ti sei mai visto? Ti aspetta una sorpresa. Ho abusato di quella funzione ogni volta che ne ho l'occasione. Così potente e utile.
Runevault,

3
Mi piace, ma c'è qualche motivo per l'ordine degli argomenti (oltre alla domanda)? Mi aspettavo [fm] come in mapper qualche motivo.
nha

2
Consiglio vivamente di usare (m vuoto) anziché {}. Quindi continuerebbe ad essere un particolare tipo di mappa.
Nickik,

96

Puoi usare 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}

So che questa risposta è stata un po 'in ritardo guardando le date, ma penso che sia perfetto.
Edwardsmatt

Questo l'ha trasformato in clojure 1.3.0?
AnnanFay,

13
la libreria generic.function è stata spostata in una libreria separata: org.clojure/algo.generic "0.1.0" l'esempio ora dovrebbe contenere: (use '[clojure.algo.generic.functor :only (fmap)]) (fmap inc {:a 1 :b 3 :c 5})
Travis Schneeberger,

2
Qualche idea su come trovare fmapin ClojureScript?
sebastibe,

37

Ecco un modo abbastanza tipico per trasformare una mappa. zipmap prende un elenco di chiavi e un elenco di valori e "fa la cosa giusta" producendo una nuova mappa Clojure. Puoi anche mettere i maptasti intorno per cambiarli, o entrambi.

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

o per concludere nella tua funzione:

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

1
Mi irrita il fatto che devo fornirgli le chiavi, ma non è un prezzo elevato da pagare. Sembra decisamente molto più bello del mio suggerimento originale.
Thomas,

1
Abbiamo la garanzia che le chiavi e i valori restituiscano i valori corrispondenti nello stesso ordine? Sia per le mappe ordinate che per le mappe hash?
Rob Lachlan,

4
Rob: sì, chiavi e val useranno lo stesso ordine per tutte le mappe - lo stesso ordine usato da un seq sulla mappa. Poiché le mappe hash, ordinate e array sono tutte immutabili, nel frattempo non è possibile che l'ordine cambi.
Chouser,

2
Questo sembra essere il caso, ma è documentato ovunque? Almeno i docstring per chiavi e val non menzionano questo. Sarei più a mio agio nell'usarlo se potessi indicare qualche documentazione ufficiale che promette che funzionerà.
Jouni K. Seppänen,

1
@Jason Sì, hanno aggiunto questo alla documentazione nella versione 1.6 Penso. dev.clojure.org/jira/browse/CLJ-1302
Jouni K. Seppänen,

23

Tratto dal ricettario di Clojure, c'è il riduttore-kv:

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

8

Ecco un modo abbastanza idiomatico per farlo:

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

Esempio:

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

2
Se non è chiaro: la funzione anon distrugge la chiave e il valore in k e v e quindi restituisce una mappatura di hash k su (fv) .
Siddhartha Reddy,

6

map-map, map-map-keysemap-map-values

Non conosco alcuna funzione esistente in Clojure per questo, ma ecco un'implementazione di quella funzione in quanto map-map-valuessei libero di copiare. Viene fornito con due funzioni strettamente correlate map-mape map-map-keys, che mancano anche nella libreria standard:

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

uso

Puoi chiamare map-map-valuescosì:

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

E le altre due funzioni come questa:

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

Implementazioni alternative

Se solo si desidera map-map-keyso map-map-values, senza la più generale map-mapfunzione, è possibile utilizzare queste implementazioni, che non si basano su 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)} )))

Inoltre, ecco un'implementazione alternativa map-mapche si basa clojure.walk/walkinvece di into, se preferisci questo fraseggio:

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

Versioni di Parellel - pmap-map, ecc.

Esistono anche versioni parallele di queste funzioni se ne hai bisogno. Usano semplicemente pmapinvece di 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
Controlla anche la funzione prismatica map-vals. È più veloce usando i transitori github.com/Prismatic/plumbing/blob/…
Clojure Quasi il

2

Sono un Clojure n00b, quindi potrebbero esserci soluzioni molto più eleganti. Ecco il mio:

(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 func crea un piccolo elenco di 2 per ogni tasto e il suo valore predefinito. Mapcat esegue questa funzione sulla sequenza dei tasti della mappa e concatena l'intero lavoro in un unico grande elenco. "applica hash-map" crea una nuova mappa da quella sequenza. (% M) può sembrare un po 'strano, è Clojure idiomatico per applicare una chiave a una mappa per cercare il valore associato.

Letture consigliate: The Clojure Cheat Sheet .


Ho pensato di seguire le sequenze come hai fatto nel tuo esempio. Mi piace anche il nome della tua funzione molto più della mia :)
Thomas

In Clojure, le parole chiave sono funzioni che si osservano in qualsiasi sequenza passino loro. Ecco perché (: parola chiave a-map) funziona. Ma usare il tasto come funzione per cercare se stesso in una mappa non funziona se il tasto non è una parola chiave. Quindi potresti voler cambiare da (% m) sopra a (m%) che funzionerà indipendentemente dalle chiavi.
Siddhartha Reddy,

Oops! Grazie per la punta, 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

Mi piace la tua reduceversione. Con una leggera variazione, può anche conservare il tipo di strutture dei record:

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

Il è {}stato sostituito da m. Con tale modifica, i record rimangono record:

(defrecord Person [firstname lastname])

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

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

Iniziando {}, il record perde la sua capacità di registrazione , che si potrebbe desiderare di conservare, se si desiderano le capacità di registrazione (ad esempio la rappresentazione della memoria compatta).


0

Mi chiedo perché nessuno abbia ancora menzionato la biblioteca dello spettro . È stato scritto per rendere questo tipo di trasformazione facile da codificare (e, ancora più importante, il codice scritto facile da capire), pur essendo molto performante:

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

Scrivere una tale funzione in Clojure puro è semplice, ma il codice diventa molto più complicato quando si passa a codice altamente nidificato composto da diverse strutture di dati. Ed è qui che entra in gioco lo spettro .

Consiglio di guardare questo episodio su Clojure TV che spiega la motivazione dietro e i dettagli dello spettro .


0

Clojure 1.7 ( rilasciato il 30 giugno 2015) offre una soluzione elegante per questo con 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"])
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.