Reputation: 59
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
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
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
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
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