Zuordnen einer Funktion zu den Werten einer Karte in Clojure


138

Ich möchte eine Wertekarte mit denselben Tasten in eine andere Karte umwandeln, jedoch mit einer Funktion, die auf die Werte angewendet wird. Ich würde denken, dass es in der Clojure-API eine Funktion dafür gibt, aber ich konnte sie nicht finden.

Hier ist eine Beispielimplementierung dessen, wonach ich suche

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

Weiß jemand, ob es map-function-on-map-valsbereits existiert? Ich würde denken, dass es so ist (wahrscheinlich auch mit einem schöneren Namen).

Antworten:


153

Ich mag deine reduceVersion ganz gut. Ich finde es idiomatisch. Hier ist eine Version, die sowieso das Listenverständnis verwendet.

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

1
Ich mag diese Version, weil sie sehr kurz und offensichtlich ist, wenn Sie alle Funktionen und solche verstehen, die sie verwendet. Und wenn Sie es nicht tun, ist es eine Ausrede, sie zu lernen!
Runevault

Genau. Ich kannte die In-Funktion nicht, aber es macht durchaus Sinn, sie hier zu verwenden.
Thomas

Oh Mann, in den du nicht gesehen hast? Sie werden verwöhnt. Ich missbrauche diese Funktion zum Teufel bei jeder Gelegenheit, die ich bekomme. So mächtig und nützlich.
Runevault

3
Ich mag es, aber gibt es einen Grund für die Reihenfolge der Argumente (neben der Frage)? Ich hatte [fm] mapaus irgendeinem Grund erwartet .
nha

2
Ich empfehle dringend, (leeres m) anstelle von {} zu verwenden. Es würde also weiterhin eine bestimmte Art von Karte sein.
Nickik

96

Sie können Folgendes verwenden 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}

Ich weiß, dass diese Antwort etwas spät war, als ich mir die Daten ansah, aber ich denke, sie ist genau richtig.
Edwardmatt

Hat es das in Clojure 1.3.0 geschafft?
AnnanFay

13
Die generic.function lib wurde in eine separate Bibliothek verschoben: org.clojure/algo.generic "0.1.0" Das Beispiel sollte nun lauten: (use '[clojure.algo.generic.functor :only (fmap)]) (fmap inc {:a 1 :b 3 :c 5})
Travis Schneeberger

2
Irgendeine Idee, wie man fmapin ClojureScript findet?
Sebastibe

37

Hier ist ein ziemlich typischer Weg, um eine Karte zu transformieren. zipmap nimmt eine Liste von Schlüsseln und eine Liste von Werten und "macht das Richtige", um eine neue Clojure-Karte zu erstellen. Sie können auch mapdie Tasten um die Tasten legen, um sie zu ändern, oder beides.

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

oder um es in Ihre Funktion einzupacken:

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

1
Es irritiert mich, dass ich die Schlüssel dafür liefern muss, aber es ist kein hoher Preis zu zahlen. Es sieht definitiv viel schöner aus als mein ursprünglicher Vorschlag.
Thomas

1
Ist uns garantiert, dass Schlüssel und Werte die entsprechenden Werte in derselben Reihenfolge zurückgeben? Für sortierte Karten und Hash-Karten?
Rob Lachlan

4
Rob: Ja, Schlüssel und Werte verwenden für alle Karten dieselbe Reihenfolge - dieselbe Reihenfolge wie in einer Sequenz auf der Karte. Da Hash-, sortierte und Array-Maps unveränderlich sind, besteht keine Chance, dass sich die Reihenfolge in der Zwischenzeit ändert.
Chouser

2
Das scheint der Fall zu sein, aber ist es irgendwo dokumentiert? Zumindest die Dokumentzeichenfolgen für Schlüssel und Werte erwähnen dies nicht. Ich würde mich damit wohler fühlen, wenn ich auf eine offizielle Dokumentation verweisen könnte, die verspricht, dass es funktionieren wird.
Jouni K. Seppänen

1
@ Jason Ja, sie haben dies der Dokumentation in Version 1.6 hinzugefügt, denke ich. dev.clojure.org/jira/browse/CLJ-1302
Jouni K. Seppänen

23

Aus dem Clojure-Kochbuch entnommen, gibt es reduziertes kv:

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

8

Hier ist ein ziemlich idiomatischer Weg, dies zu tun:

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

Beispiel:

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

2
Wenn es nicht klar ist: Die anon-Funktion zerstört den Schlüssel und den Wert in k und v und gibt dann eine Hash-Map-Zuordnung k zu (fv) zurück .
Siddhartha Reddy

6

map-map, map-map-keysUndmap-map-values

Ich kenne keine existierende Funktion in Clojure dafür, aber hier ist eine Implementierung dieser Funktion, da map-map-valuesSie frei kopieren können. Es kommt mit zwei eng verwandten Funktionen map-mapund map-map-keys, die auch in der Standardbibliothek fehlen:

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

Verwendung

Sie können so anrufen map-map-values:

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

Und die anderen beiden Funktionen wie folgt:

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

Alternative Implementierungen

Wenn Sie nur map-map-keysoder map-map-valuesohne die allgemeinere map-mapFunktion möchten , können Sie diese Implementierungen verwenden, die nicht auf Folgendem beruhen 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)} )))

Hier ist auch eine alternative Implementierung map-map, die auf basiert, clojure.walk/walkanstatt into, wenn Sie diese Formulierung bevorzugen:

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

Parellel-Versionen - pmap-mapusw.

Es gibt auch parallele Versionen dieser Funktionen, wenn Sie sie benötigen. Sie verwenden einfach pmapanstelle von 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
Überprüfen Sie auch die Funktion prismatics map-vals. Es ist schneller mit Transienten github.com/Prismatic/plumbing/blob/…
ClojureMostly

2

Ich bin ein Clojure n00b, daher gibt es möglicherweise viel elegantere Lösungen. Hier ist meins:

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

Die anon func erstellt aus jedem Schlüssel und seinem angegebenen Wert eine kleine 2-Liste. Mapcat führt diese Funktion über die Reihenfolge der Tasten der Karte aus und verkettet die gesamten Werke zu einer großen Liste. "Hash-Map anwenden" erstellt aus dieser Sequenz eine neue Map. Das (% m) mag etwas seltsam aussehen. Es ist eine idiomatische Clojure, um einen Schlüssel auf eine Karte anzuwenden, um den zugehörigen Wert nachzuschlagen.

Am meisten empfohlene Lektüre: The Clojure Cheat Sheet .


Ich habe darüber nachgedacht, Sequenzen zu durchlaufen, wie Sie es in Ihrem Beispiel getan haben. Ich mag auch den Namen deiner Funktion viel mehr als meinen eigenen :)
Thomas

In Clojure sind Schlüsselwörter Funktionen, die sich in der Reihenfolge nachschlagen, in der sie an sie übergeben werden. Deshalb funktioniert (: Schlüsselwort a-map). Die Verwendung des Schlüssels als Funktion zum Nachschlagen in einer Karte funktioniert jedoch nicht, wenn der Schlüssel kein Schlüsselwort ist. Vielleicht möchten Sie die obigen Werte (% m) in (m%) ändern, was unabhängig von den Tasten funktioniert.
Siddhartha Reddy

Hoppla! Danke für den Tipp, 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

Ich mag deine reduceVersion. Mit einer sehr geringen Abweichung kann auch die Art der Datensatzstrukturen beibehalten werden:

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

Das {}wurde ersetzt durch m. Mit dieser Änderung bleiben Datensätze Datensätze:

(defrecord Person [firstname lastname])

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

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

Zu Beginn {}verliert der Datensatz seine Aufzeichnungsfähigkeit , die möglicherweise beibehalten werden soll, wenn Sie die Aufzeichnungsfunktionen wünschen (z. B. kompakte Speicherdarstellung).


0

Ich frage mich, warum noch niemand die Gespensterbibliothek erwähnt hat . Es wurde geschrieben, um diese Art der Transformation einfach zu codieren (und, was noch wichtiger ist, den geschriebenen Code leicht zu verstehen), während es dennoch sehr performant ist:

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

Das Schreiben einer solchen Funktion in reinem Clojure ist einfach, aber der Code wird viel schwieriger, wenn Sie zu hoch verschachteltem Code übergehen, der aus verschiedenen Datenstrukturen besteht. Und hier kommt das Gespenst ins Spiel .

Ich empfehle, diese Episode auf Clojure TV zu sehen, in der die Motivation und die Details des Gespenstes erklärt werden .


0

Clojure 1.7 ( veröffentlicht am 30. Juni 2015) bietet hierfür eine elegante Lösung mit 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"])
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.