BobbyPin
BobbyPin

Reputation: 59

Clojure - Updating Each Inner Map in Nested Map

Say I have a nested map structure, such as

{:val1 {:m1 1 :m2 2 :m3 2} :val2 {:m1 4 :m2 8 :m3 7}}

This example only has two values, but in general there could be more. I know that the keys are the same for each of the nested maps (:m1, :m2, and :m3 in the example above). I have a list of keywords, say

[:m1 :m3]

and I would like to divide the value of each inner map by some number, say 5, for each of the key words given in the list. Continuing with my example, I want to get

{:val1 {:m1 1/5 :m2 2 :m3 2/5} :val2 {:m1 4/5 :m2 8 :m3 7/5}}

How can I do this? For a fixed inner key such as :m1, I can do

(map #(update-in % [1 :m1] / 5) nested-map)

But I'm not sure how to generalize this to a list of keywords. Thanks!

Upvotes: 1

Views: 1124

Answers (4)

Chris Murphy
Chris Murphy

Reputation: 6509

You could first of all define a function that works at the detail level:

(let [interesting-keys (set [:m1 :m3])]
  (defn apply-effect [[k v]]
    (if (interesting-keys k)
      [k (/ v 5)]
      [k v])))

As you can see some map entry values will be transformed while others will be left as they were.

Your example input data:

(def data {:val1 {:m1 1 :m2 2 :m3 2} :val2 {:m1 4 :m2 8 :m3 7}})

So here we look over the outer structure in order to apply a detail function f:

(defn make-changes [m f]
  (->> m
       (map (fn [[k v]]
              [k (->> v
                      (map f)
                      (into {}))]))
       (into {})))

Of course f will be apply-effect:

(make-changes data apply-effect) 
;;=> {:val1 {:m1 1/5, :m2 2, :m3 2/5}, :val2 {:m1 4/5, :m2 8, :m3 7/5}}

Look carefully to see that the make-changes function above used this abstraction twice:

(defn map-map-entries [f m]
  (->> m
       (map f)
       (into {})))

So we can now answer the question using map-map-entries:

(map-map-entries
  (fn [[k v]]
    [k (map-map-entries
         apply-effect
         v)])
  data)

Upvotes: 0

Thumbnail
Thumbnail

Reputation: 13473

In the footsteps of the Clojure Cookbook, I'd define

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

... then use core functions to do what you want:

(defn map-inner-keys-with [ks f m]
  (map-vals
    (fn [vm] (into vm (map (juxt identity (comp f vm)) ks)))
    m))

For example,

(map-inner-keys-with [:m1 :m3] #(/ % 5)
                     {:val1 {:m1 1 :m2 2 :m3 2}
                      :val2 {:m1 4 :m2 8 :m3 7}})
=> {:val1 {:m1 1/5, :m2 2, :m3 2/5}, :val2 {:m1 4/5, :m2 8, :m3 7/5}}

Upvotes: 1

Minh Tuan Nguyen
Minh Tuan Nguyen

Reputation: 1054

You can use specter to do this transformation:

(:require [clojure.test :refer :all]
          [com.rpl.specter :as specter])

(deftest ^:focused transform-test
   (let [selectors #{:m1 :m3}
         input {:val1 {:m1 1 :m2 2 :m3 2} :val2 {:m1 4 :m2 8 :m3 7}}
         output {:val1 {:m1 1/5 :m2 2 :m3 2/5} :val2 {:m1 4/5 :m2 8 :m3 7/5}}]

         (is (= output
               (specter/transform [specter/MAP-VALS
                                   specter/ALL
                                   #(contains?  selectors (first %))
                                   specter/LAST]
                                   #(/ % 5)
                                   input)))))

Upvotes: 0

Solaxun
Solaxun

Reputation: 2792

Here is an answer that will work, assuming that your level of nesting is constant (which per your example, seems like a fair assumption). Note that I provide the keys as a set, if you want to use a vector you can simply modify the function to bind a symbol to a hash-set call in a let at the top of the function.

(defn change-map [m f ks]
  (for [[k1 v1] m]
       (hash-map k1 (into {} (for [[k2 v2] v1]
                                  (if (contains? ks k2) [k2 (f v2)]
                                      [k2 v2]))))))

(change-map example #{:m1 :m3})

Or if you would prefer to just pass in keys one at a time:

(defn change-map [m f & ks]
  (let [ks (apply hash-set ks)]
       (for [[k1 v1] m]
            (hash-map k1 (into {} (for [[k2 v2] v1]
                                       (if (contains? ks k2) [k2 (f v2)]
                                           [k2 v2])))))))

(change-map example #(/ % 2) :m1 :m3)

After some thought, the above examples didn't sit well with me. Not that they are wrong, just felt too over-engineered. I think the below is closer to what you had in mind, and is considerably more succinct. You can obviously generalize this by changing the hard coded / 2 to be a function.

(into {} (map (fn[[k v]] {k (reduce #(update %1 %2 / 2) v [:m1 :m3])}) example))

Upvotes: 0

Related Questions