简体   繁体   中英

How to select keys in nested maps in Clojure?

Let's say I have a map ( m ) like this:

(def m {:a 1 :b 2 :c {:d 3 :e 4} :e { ... } ....})

I'd like to create a new map only containing :a , :b and :d from m , ie the result should be:

{:a 1 :b 2 :d 3}

I know that I can use select-keys to easily get :a and :b :

(select-keys m [:a :b])

But what's a good way to also get :d ? I'm looking for something like this:

(select-keys* m [:a :b [:c :d]])

Does such a function exists in Clojure or what's the recommended approach?

In pure Clojure I would do it like this:

(defn select-keys* [m paths]
  (into {} (map (fn [p]
                  [(last p) (get-in m p)]))
        paths))

(select-keys* m [[:a] [:b] [:c :d]]) ;;=> {:a 1, :b 2, :d 3}

I prefer keeping the type of a path regular, so a sequence of keys for all paths. In clojure.spec this would read as

(s/def ::nested-map (s/map-of keyword? 
                              (s/or :num number? :map ::nested-map)))
(s/def ::path (s/coll-of keyword?))
(s/fdef select-keys*
        :args (s/cat :m ::nested-map 
                     :paths (s/coll-of ::path)))

I'm not aware of such a function being part of Clojure. You'll probably have to write it yourself. I've came up with this :

(defn select-keys* [m v]
  (reduce 
    (fn [aggregate next]
      (let [key-value (if (vector? next)
                        [(last next)
                         (get-in m next)]
                        [next
                         (get m next)])]
        (apply assoc aggregate key-value)))
    {}
    v))

As an alternative you can use destructing on a function, for example:

(def m {:a 1 :b 2 :c {:d 3 :e 4}})

(defn get-m
  [{a :a b :b {d :d} :c}]
  {:a 1 :b b :d d})

(get-m m) ; => {:a 1, :b 2, :d 3}

You can use clojure.walk .

(require '[clojure.walk :as w])

(defn nested-select-keys
  [map keyseq]
  (w/postwalk (fn [x]
                (if (map? x)
                  (select-keys x keyseq)
                  (identity x))) map))

(nested-select-keys {:a 1 :b {:c 2 :d 3}} [:a :b :c])
  ; => {:a 1, :b {:c 2}}

Require paths to be vectors so you can use peek (much faster than last ). Reduce over the paths like this:

(defn select-keys* [m paths] 
  (reduce (fn [r p] (assoc r (peek p) (get-in m p))) {} paths))

(select-keys* m [[:a] [:b] [:c :d]]) ;;=> {:a 1, :b 2, :d 3}

Of course, this assumes that all your terminal keys are unique.

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