Cartographie d'une fonction sur les valeurs d'une carte dans Clojure

138

Je veux transformer une carte de valeurs en une autre carte avec les mêmes clés mais avec une fonction appliquée aux valeurs. Je penserais qu'il y avait une fonction pour faire cela dans l'api clojure, mais je n'ai pas pu la trouver.

Voici un exemple d'implémentation de ce que je recherche

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

Est-ce que quelqu'un sait s'il map-function-on-map-valsexiste déjà? Je pense que c'est le cas (probablement avec un nom plus joli aussi).

Thomas
la source

Réponses:

153

J'aime bien ta reduceversion. Je pense que c'est idiomatique. Voici une version utilisant de toute façon la compréhension de liste.

(defn foo [m f]
  (into {} (for [[k v] m] [k (f v)])))
Brian Carper
la source
1
J'aime cette version car elle est super courte et évidente si l'on comprend toutes les fonctions et telles qu'elle utilise. Et si vous ne le faites pas, c'est une excuse pour les apprendre!
Runevault
Je suis d'accord. Je ne connaissais pas la fonction into, mais il est parfaitement logique de l'utiliser ici.
Thomas
Oh mec que tu n'avais pas vu? Vous allez vous régaler. J'abuse de cette fonction chaque fois que j'en ai l'occasion. Si puissant et utile.
Runevault
3
Je l'aime bien, mais y a-t-il une raison pour l'ordre des arguments (à part la question)? Je m'attendais à [fm] comme dansmap pour une raison quelconque.
nha
2
Je recommande fortement d'utiliser (m vide) plutôt que {}. Cela continuerait donc d'être un type particulier de carte.
nickik
96

Vous pouvez utiliser 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
la source
Je sais que cette réponse était un peu tardive en regardant les dates, mais je pense qu'elle est parfaite.
edwardsmatt
Est-ce que cela est devenu clojure 1.3.0?
AnnanFay
13
la bibliothèque generic.function a été déplacée vers une bibliothèque séparée: org.clojure/algo.generic "0.1.0" l'exemple devrait maintenant se lire: (use '[clojure.algo.generic.functor :only (fmap)]) (fmap inc {:a 1 :b 3 :c 5})
Travis Schneeberger
2
Une idée comment trouver fmapdans ClojureScript?
sebastibe
37

Voici une manière assez typique de transformer une carte. zipmap prend une liste de clés et une liste de valeurs et "fait ce qu'il faut" en produisant une nouvelle carte Clojure. Vous pouvez également placer les maptouches autour des touches pour les changer, ou les deux.

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

ou pour conclure dans votre fonction:

(defn map-function-on-map-vals [m f]
    (zipmap (keys m) (map f (vals m))))
Arthur Ulfeldt
la source
1
Cela m'irrite de devoir fournir les clés, mais ce n'est pas un prix élevé à payer. Cela a définitivement l'air beaucoup plus agréable que ma suggestion initiale.
Thomas
1
Sommes-nous garantis que les clés et les vals renvoient les valeurs correspondantes dans le même ordre? Pour les cartes triées et les cartes de hachage?
Rob Lachlan
4
Rob: oui, les clés et les vals utiliseront le même ordre pour toutes les cartes - le même ordre qu'un seq sur la carte utilise. Étant donné que les cartes de hachage, triées et matricielles sont toutes immuables, il n'y a aucune chance que l'ordre change dans l'intervalle.
Chouser
2
Cela semble être le cas, mais est-ce documenté quelque part? Au moins les docstrings pour les clés et les vals ne le mentionnent pas. Je serais plus à l'aise d'utiliser cela si je pouvais indiquer une documentation officielle qui promet que cela fonctionnera.
Jouni K. Seppänen
1
@Jason Oui, ils ont ajouté cela à la documentation de la version 1.6 je pense. dev.clojure.org/jira/browse/CLJ-1302
Jouni K. Seppänen
23

Tiré du livre de cuisine Clojure, il y a réduire-kv:

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

Voici une façon assez idiomatique de faire cela:

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

Exemple:

user> (map-function-on-map-vals {1 1, 2 2, 3 3} inc))
{3 4, 2 3, 1 2}
Siddhartha Reddy
la source
2
Si ce n'est pas clair: la fonction anon déstructure la clé et la valeur en k et v puis retourne un mappage de hash-map k vers (fv) .
Siddhartha Reddy
6

map-map, map-map-keysetmap-map-values

Je ne connais aucune fonction existante dans Clojure pour cela, mais voici une implémentation de cette fonction car map-map-valuesvous êtes libre de copier. Il est livré avec deux fonctions étroitement liées, map-mapet map-map-keys, qui sont également absentes de la bibliothèque 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) )

Usage

Vous pouvez appeler map-map-valuescomme ceci:

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

Et les deux autres fonctions comme celle-ci:

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

Implémentations alternatives

Si vous voulez seulement map-map-keysou map-map-values, sans la map-mapfonction plus générale , vous pouvez utiliser ces implémentations, qui ne reposent pas sur 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)} )))

En outre, voici une implémentation alternative de map-mapqui est basée sur clojure.walk/walkau lieu de into, si vous préférez cette formulation:

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

Versions parallèles - pmap-map, etc.

Il existe également des versions parallèles de ces fonctions si vous en avez besoin. Ils utilisent simplement à la pmapplace 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
la source
1
Vérifiez également la fonction map-vals prismatiques. C'est plus rapide en utilisant les transitoires github.com/Prismatic/plumbing/blob/…
ClojureMostly
2

Je suis un Clojure n00b, il peut donc y avoir des solutions bien plus élégantes. Voici la mienne:

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

La fonction anon fait une petite liste de 2 à partir de chaque clé et de sa valeur f'ed. Mapcat exécute cette fonction sur la séquence des touches de la carte et concatène l'ensemble des travaux en une seule grande liste. "apply hash-map" crée une nouvelle carte à partir de cette séquence. Le (% m) peut sembler un peu bizarre, c'est Clojure idiomatique pour appliquer une clé à une carte pour rechercher la valeur associée.

Lecture la plus fortement recommandée: The Clojure Cheat Sheet .

Carl Smotricz
la source
J'ai pensé à passer par des séquences comme vous l'avez fait dans votre exemple. J'aime aussi le nom de votre fonction beaucoup plus que le mien :)
Thomas
Dans Clojure, les mots-clés sont des fonctions qui se recherchent dans la séquence qui leur est passée. C'est pourquoi (: keyword a-map) fonctionne. Mais utiliser la clé comme fonction pour se rechercher dans une carte ne fonctionne pas si la clé n'est pas un mot-clé. Vous voudrez peut-être changer le (% m) ci-dessus en (m%) qui fonctionnera quelles que soient les clés.
Siddhartha Reddy
Oups! Merci pour la pointe, 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)))
Didier A.
la source
1

J'aime ta reduceversion. Avec une très légère variation, il peut également conserver le type de structures d'enregistrements:

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

Le a {}été remplacé par m. Avec ce changement, les enregistrements restent des enregistrements:

(defrecord Person [firstname lastname])

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

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

En commençant par {}, l'enregistrement perd son caractère d' enregistrement , que l'on peut souhaiter conserver, si vous désirez les capacités d'enregistrement (représentation de mémoire compacte par exemple).

Olange
la source
0

Je me demande pourquoi personne n'a mentionné le spectre bibliothèque de . Il a été écrit pour rendre ce type de transformation facile à coder (et, plus important encore, le code écrit facile à comprendre), tout en étant toujours très performant:

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

Écrire une telle fonction dans Clojure pur est simple, mais le code devient beaucoup plus délicat une fois que vous passez à un code hautement imbriqué composé de différentes structures de données. Et c'est là que spectre entre en jeu.

Je recommande de regarder cet épisode sur Clojure TV qui explique la motivation derrière et les détails du spectre .

mzuther
la source
0

Clojure 1.7 ( publié le 30 juin 2015) offre une solution élégante pour cela avec 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
la source