Search code examples
dictionaryclojurenested

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.


Solution

  • 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)])))