Asignación de una función a los valores de un mapa en Clojure

138

Quiero transformar un mapa de valores en otro mapa con las mismas teclas pero con una función aplicada a los valores. Creo que había una función para hacer esto en la api clojure, pero no he podido encontrarla.

Aquí hay un ejemplo de implementación de lo que estoy buscando

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

¿Alguien sabe si map-function-on-map-valsya existe? Creo que sí (probablemente con un nombre más agradable también).

Thomas
fuente

Respuestas:

153

Me gusta tu reduceversión muy bien. Creo que es idiomático. Aquí hay una versión que usa la comprensión de la lista de todos modos.

(defn foo [m f]
  (into {} (for [[k v] m] [k (f v)])))
Brian Carper
fuente
1
Me gusta esta versión porque es súper corta y obvia si comprendes todas las funciones y las que usa. Y si no, ¡es una excusa para aprenderlos!
Runevault el
Estoy de acuerdo. No conocía la función into, pero tiene mucho sentido usarla aquí.
Thomas el
Oh hombre que no habías visto? Estás de enhorabuena. Abuso de esa función cada vez que tengo la oportunidad. Tan poderoso y útil.
Runevault el
3
Me gusta, pero ¿hay alguna razón para el orden de los argumentos (además de la pregunta)? Esperaba [fm] como mappor alguna razón.
nha
2
Recomiendo usar (m vacío) en lugar de {}. Por lo tanto, seguiría siendo un tipo particular de mapa.
nickik
96

Puedes usar el 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}
Arthur Edelstein
fuente
Sé que esta respuesta tardó un poco en ver las fechas, pero creo que es acertada.
edwardsmatt
¿Esto lo convirtió en clojure 1.3.0?
AnnanFay
13
la lib genérica de la función se ha movido a una biblioteca separada: org.clojure/algo.generic "0.1.0" el ejemplo ahora debería leer: (use '[clojure.algo.generic.functor :only (fmap)]) (fmap inc {:a 1 :b 3 :c 5})
Travis Schneeberger
2
¿Alguna idea de cómo encontrar fmapen ClojureScript?
sebastibe
37

Aquí hay una forma bastante típica de transformar un mapa. zipmap toma una lista de claves y una lista de valores y "hace lo correcto" produciendo un nuevo mapa de Clojure. También puede poner mapalrededor de las teclas para cambiarlas, o ambas.

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

o para envolverlo en su función:

(defn map-function-on-map-vals [m f]
    (zipmap (keys m) (map f (vals m))))
Arthur Ulfeldt
fuente
1
Me irrita que tenga que proporcionarle las llaves, pero no es un precio alto a pagar. Definitivamente se ve mucho mejor que mi sugerencia original.
Thomas el
1
¿Estamos garantizados de que las claves y los valores devuelven los valores correspondientes en el mismo orden? ¿Tanto para mapas ordenados como para mapas hash?
Rob Lachlan el
44
Rob: sí, las llaves y los vals usarán el mismo orden para todos los mapas, el mismo orden que usa una secuencia en el mapa. Dado que los mapas hash, ordenados y de matriz son todos inmutables, no hay posibilidad de que el orden cambie mientras tanto.
Chouser el
2
Ese parece ser el caso, pero ¿está documentado en alguna parte? Al menos las cadenas de documentos para claves y valles no mencionan esto. Me sentiría más cómodo usando esto si pudiera señalar alguna documentación oficial que prometa que va a funcionar.
Jouni K. Seppänen
1
@ Jason Sí, creo que agregaron esto a la documentación en la versión 1.6. dev.clojure.org/jira/browse/CLJ-1302
Jouni K. Seppänen
23

Tomado del Clojure Cookbook, hay reduce-kv:

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

Aquí hay una forma bastante idiomática de hacer esto:

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

Ejemplo:

user> (map-function-on-map-vals {1 1, 2 2, 3 3} inc))
{3 4, 2 3, 1 2}
Siddhartha Reddy
fuente
2
Si no está claro: la función anon destruye la clave y el valor a k y v y luego devuelve una asignación de mapa hash k a (fv) .
Siddhartha Reddy el
6

map-map` map-map-keys` ymap-map-values

No sé de ninguna función existente en Clojure para esto, pero aquí hay una implementación de esa función, ya map-map-valuesque puede copiarla libremente. Viene con dos funciones estrechamente relacionadas map-mapy map-map-keysque también faltan en la biblioteca estándar:

(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

Puedes llamar map-map-valuesasí:

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

Y las otras dos funciones como esta:

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

Implementaciones alternativas

Si solo desea map-map-keyso map-map-values, sin la map-mapfunción más general , puede usar estas implementaciones, que no dependen de 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)} )))

Además, aquí hay una implementación alternativa map-mapque se basa en clojure.walk/walklugar de into, si prefiere esta redacción:

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

Versiones paralelas pmap-map, etc.

También hay versiones paralelas de estas funciones si las necesita. Simplemente usan en pmaplugar de 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) )
Rory O'Kane
fuente
1
También verifique la función prismática map-vals. Es más rápido usar transitorios github.com/Prismatic/plumbing/blob/…
ClojureMás tarde
2

Soy un Clojure n00b, por lo que puede haber soluciones mucho más elegantes. Aquí está el mío:

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

El anon func hace una pequeña lista de 2 de cada tecla y su valor f'ed. Mapcat ejecuta esta función sobre la secuencia de las teclas del mapa y concatena todos los trabajos en una gran lista. "aplicar hash-map" crea un nuevo mapa a partir de esa secuencia. El (% m) puede parecer un poco extraño, es Clojure idiomático para aplicar una clave a un mapa para buscar el valor asociado.

Lectura más recomendada: La hoja de trucos de Clojure .

Carl Smotricz
fuente
Pensé en pasar por secuencias como lo hiciste en tu ejemplo. También me gusta el nombre de tu función mucho más que el mío :)
Thomas, el
En Clojure, las palabras clave son funciones que se buscan en cualquier secuencia que se les pase. Es por eso que (: palabra clave a-map) funciona. Pero usar la clave como una función para buscar en un mapa no funciona si la clave no es una palabra clave. Por lo tanto, es posible que desee cambiar el (% m) anterior a (m%) que funcionará sin importar cuáles sean las claves.
Siddhartha Reddy el
¡Uy! Gracias por el consejo, Siddhartha!
Carl Smotricz el
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)))
Didier A.
fuente
1

Me gusta tu reduceversión Con una variación muy leve, también puede retener el tipo de estructuras de registros:

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

El {}fue reemplazado por m. Con ese cambio, los registros siguen siendo registros:

(defrecord Person [firstname lastname])

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

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

Al comenzar {}, el registro pierde su capacidad de grabación , que es posible que desee conservar, si desea las capacidades de registro (representación de memoria compacta, por ejemplo).

olange
fuente
0

Me pregunto por qué nadie ha mencionado la biblioteca de espectros todavía. Se ha escrito para que este tipo de transformación sea fácil de codificar (y, lo que es más importante, el código escrito sea fácil de entender), sin dejar de ser muy eficaz:

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

Escribir una función de este tipo en Clojure puro es simple, pero el código se vuelve mucho más complicado una vez que pasa a un código altamente anidado compuesto por diferentes estructuras de datos. Y aquí es donde entra el espectro .

Recomiendo ver este episodio en Clojure TV que explica la motivación y los detalles del espectro .

mzuther
fuente
0

Clojure 1.7 ( lanzado el 30 de junio de 2015) proporciona una solución elegante para esto 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"])
Jack Pendleton
fuente