简体   繁体   中英

Clojure: How to apply a function to every value in a nested map and update?

Let's say there is a nested map like below: (partially nested only)

(def mymap {:a 10
        :b {:ba 21, :bb 22 :bc 23}
        :c 30
        :d {:da 41, :db 42}})

How can I apply a function, say #(* % 2) , and update every value in this map? That is without specifying any key. The result will look like this:

{:a 20, 
 :b {:ba 42, :bb 44, :bc 46}, 
 :c 60, 
 :d {:da 82, :db 84}}

So far, I came up with this own function:

(defn map-kv [f coll] (reduce-kv (fn [m k v] (assoc m k (f v))) (empty coll) coll))

But I still need to specify a first-level key and can't apply to all first-level and second-level keys values.

You may wish to review the postwalk function: https://clojuredocs.org/clojure.walk/postwalk

(def data
   {:a 10
    :b {:ba 21, :bb 22 :bc 23}
    :c 30
    :d {:da 41, :db 42}} )

(defn tx-nums [x]
  (if (number? x)
    (* 2 x)
    x))

(postwalk tx-nums data) => 
  {:a 20, 
   :b {:ba 42, :bb 44, :bc 46}, 
   :c 60, 
   :d {:da 82, :db 84}}

Porthos3 makes a good point. The above will transform map keys as well as map values. If you want only values to change, you could use the map-vals function from the Tupelo Clojure library (the Medley lib has a similar function ).

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test)
  (:require
    [tupelo.core :as t]
    [clojure.walk :as walk]))

(dotest
  (let [data-2     {1 2
                    3 4}
        tx-vals-fn (fn [item]
                     (if (map? item)
                       (t/map-vals item #(* 2 %))
                       item))
        result     (walk/postwalk tx-vals-fn data-2)]
    (is= (spyx result) {1 4, 3 8})))

with result:

-------------------------------
   Clojure 1.10.1    Java 13
-------------------------------

Testing tst.demo.core
result => {1 4, 3 8}

Ran 2 tests containing 1 assertions.
0 failures, 0 errors.

In addition to postwalk, as Alan mentioned, it is trivial to recursively explore the map and update every key. Clojure provides a function called fmap that simply applies a function to every value in a map. To use:

In project.clj, declare this dependency:

[org.clojure/algo.generic "0.1.2"]

And in your code, then require:

(require '[clojure.algo.generic.functor :as f :only [fmap]])

Then define a function that will walk your map recursively:

(defn fmap*
  [f m]
  (f/fmap #(if (map? %)
             (fmap* f %)
             (f %))
          m))

(fmap*
   (partial * 2) ;; double every number
   {:a 21 :b {:x 11 :y 22 :z {:p 100 :q 200}}})
=> {:a 42, :b {:x 22, :y 44, :z {:p 200, :q 400}}}

In case you don't want to have to include a non-core function, here's the code for fmap used on a map, from the clojure source (adapted for a defn):

(defn fmap [f m]
  (into (empty m) (for [[k v] m] [k (f v)])))

I really like specter, see https://github.com/nathanmarz/specter

If you exactly want to change the top 2 levels, calling transform twice is the simplest

(->> mymap 
     (sp/transform [sp/MAP-VALS map? sp/MAP-VALS number?] #(* 2 %))
     (sp/transform [sp/MAP-VALS number?] #(* 2 %)))

You can implement the walk part in specter too, if you really want to replace everything recursively. For example, I wanted to floatify all numbers in an arbitrary structure. First, I had to define the walker (which also handles vectors, seq, and sets). This is generic, so I can reuse it.

 (defprotocolpath WalkValues)

 (extend-protocolpath WalkValues
                 clojure.lang.IPersistentVector [ALL WalkValues]
                 clojure.lang.IPersistentMap [MAP-VALS WalkValues]
                 clojure.lang.IPersistentSet [ALL WalkValues]
                 clojure.lang.ISeq [ALL WalkValues]
                 Object STAY)

but once I done that, I can implement it is

 (sp/transform [sp/WalkValues integer?] float mymap)

or in this example

 (sp/transform [sp/WalkValues number?] #(* 2 %) mymap)
(require '[clojure.walk :as walk])

(defn fmap [f m]
  (into (empty m) (for [[k v] m] [k (f v)])))

(defn map-leaves
  [f form]
  (walk/postwalk (fn [m]
                   (if (map? m)
                     (fmap #(if (map? %) % (f %)) m)
                     m))
                 form))

example:

(map-leaves
 (partial * 2)
 {:a 10
  :b {:ba 21, :bb 22 :bc 23}
  :c 30
  :d {:da 41, :db 42}})
;; {:a 20, :b {:ba 42, :bb 44, :bc 46}, :c 60, :d {:da 82, :db 84}}

explanation:

postwalk calls walk in its implementation.

(defn postwalk
  [f form]
  (walk (partial postwalk f) f form))

walk checks the type of the form and it matches the form (a map) against coll? and then maps inner (which is postwalk with f) against the form which matches map-entry? .

We don't want to "postwalk with f" against the key so we check to see if it's a map and skip it (return m) if it's not a map. (This logic fails if you use a map as a key.)

postwalk passed our f into walk as outer . The lambda inside map-leaves skips calling outer (aka f ) on the resulting maps (look at the coll? match) as it backs out of the recursion. The maps were already transformed by the map inner .

(defn walk
  [inner outer form]
  (cond
    (list? form)      (outer (apply list (map inner form)))
    (map-entry? form)
    (outer (MapEntry. (inner (key form)) (inner (val form)) nil))
    (seq? form)       (outer (doall (map inner form)))
    (record? form)    (outer (reduce (fn [r x] (conj r (inner x))) form form))
    (coll? form)      (outer (into (empty form) (map inner form)))
    :else             (outer form)))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM