In my Clojure webapp I have various model namespaces with functions that take a map as an agrument and somehow insert that map into a database. I would like to be able take out only the desired keys from the map before I do the insert.
A basic example of this is:
(let [msg-keys [:title :body]
msg {:title "Hello" :body "This is an example" :somekey "asdf" :someotherkey "asdf"}]
(select-keys msg msg-keys))
;; => {:title "Hello" :body "This is an example"}
select-keys
is not an option when the map is somewhat complex and I would like to select a specific set of nested keys:
(let [person {:name {:first "john" :john "smith"} :age 40 :weight 155 :something {:a "a" :b "b" :c "c" :d "d"}}]
(some-select-key-fn person [:name [:first] :something [:a :b]]))
;; => {:name {:first "john"} :something {:a "a" :b "b"}}
Is there a way to do this with the core functions? Is there a way do this purely with destructuring?
This topic was discussed in the Clojure Google Group along with a few solutions.
Destructuring is probably the closest to a "core" capability, and may be a fine solution if your problem is rather static and the map has all of the expected keys (thus avoiding nil
). It could look like:
(let [person {:name {:first "john" :john "smith"} :age 40 :weight 155 :something {:a "a" :b "b" :c "c" :d "d"}}
{{:keys [first]} :name {:keys [a b]} :something} person]
{:name {:first first} :something {:a a :b b}})
;; => {:name {:first "john"}, :something {:a "a", :b "b"}}
Below is a survey of the solutions in the Clojure Google Group thread, applied to your sample map. They each have a different take on how to specify the nested keys to be selected.
Here is Christophe Grand's solution:
(defprotocol Selector
(-select [s m]))
(defn select [m selectors-coll]
(reduce conj {} (map #(-select % m) selectors-coll)))
(extend-protocol Selector
clojure.lang.Keyword
(-select [k m]
(find m k))
clojure.lang.APersistentMap
(-select [sm m]
(into {}
(for [[k s] sm]
[k (select (get m k) s)]))))
Using it requires a slightly modified syntax:
(let [person {:name {:first "john" :john "smith"} :age 40 :weight 155 :something {:a "a" :b "b" :c "c" :d "d"}}]
(select person [{:name [:first] :something [:a :b]}]))
;; => {:something {:b "b", :a "a"}, :name {:first "john"}}
Here is Moritz Ulrich's solution (he cautions that it doesn't work on maps with seqs as keys):
(defn select-in [m keyseq]
(loop [acc {} [k & ks] (seq keyseq)]
(if k
(recur
(if (sequential? k)
(let [[k ks] k]
(assoc acc k
(select-in (get m k) ks)))
(assoc acc k (get m k)))
ks)
acc)))
Using it requires another slightly modified syntax:
(let [person {:name {:first "john" :john "smith"} :age 40 :weight 155 :something {:a "a" :b "b" :c "c" :d "d"}}]
(select-in person [[:name [:first]] [:something [:a :b]]]))
;; => {:something {:b "b", :a "a"}, :name {:first "john"}}
Here is Jay Fields's solution:
(defn select-nested-keys [m top-level-keys & {:as pairs}]
(reduce #(update-in %1 (first %2) select-keys (last %2)) (select-keys m top-level-keys) pairs))
It uses a different syntax:
(let [person {:name {:first "john" :john "smith"} :age 40 :weight 155 :something {:a "a" :b "b" :c "c" :d "d"}}]
(select-nested-keys person [:name :something] [:name] [:first] [:something] [:a :b]))
;; => {:something {:b "b", :a "a"}, :name {:first "john"}}
Here is Baishampayan Ghose's solution:
(defprotocol ^:private IExpandable
(^:private expand [this]))
(extend-protocol IExpandable
clojure.lang.Keyword
(expand [k] {k ::all})
clojure.lang.IPersistentVector
(expand [v] (if (empty? v)
{}
(apply merge (map expand v))))
clojure.lang.IPersistentMap
(expand [m]
(assert (= (count (keys m)) 1) "Number of keys in a selector map can't be more than 1.")
(let [[k v] (-> m first ((juxt key val)))]
{k (expand v)}))
nil
(expand [_] {}))
(defn ^:private extract* [m selectors expand?]
(let [sels (if expand? (expand selectors) selectors)]
(reduce-kv (fn [res k v]
(if (= v ::all)
(assoc res k (m k))
(assoc res k (extract* (m k) v false))))
{} sels)))
(defn extract
"Like select-keys, but can select nested keys.
Examples -
(extract [{:b {:c [:d]}} :g] {:a 1 :b {:c {:d 1 :e 2}} :g 42 :xxx 11})
;=> {:g 42, :b {:c {:d 1}}}
(extract [:g] {:a 1 :b {:c {:d 1 :e 2}} :g 42 :xxx 11})
;=> {:g 42}
(extract [{:b [:c]} :xxx] {:a 1 :b {:c {:d 1 :e 2}} :g 42 :xxx 11})
;=> {:b {:c {:d 1, :e 2}}, :xxx 11}
Also see - exclude"
[selectors m]
(extract* m selectors true))
It uses another syntax (and the parameters are reversed):
(let [person {:name {:first "john" :john "smith"} :age 40 :weight 155 :something {:a "a" :b "b" :c "c" :d "d"}}]
(extract [{:name [:first]} {:something [:a :b]}] person))
;; => {:name {:first "john"}, :something {:a "a", :b "b"}}