如何在Clojure的嵌套地图中选择键?

How to select keys in nested maps in Clojure?

假设我有一个这样的地图(m):

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

我想创建一个仅包含m中的:a:b:d的新地图,即结果应为:

1
{:a 1 :b 2 :d 3}

我知道我可以使用select-keys轻松获取:a:b

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

但是获得:d的好方法是什么? 我正在寻找这样的东西:

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

Clojure中是否存在这样的功能,或者推荐的方法是什么?


在纯Clojure中,我会这样做:

1
2
3
4
5
6
(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}

我更喜欢保持路径类型的规则性,以便为所有路径提供一系列键。 在clojure.spec中,它表示为

1
2
3
4
5
6
(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)))


或者,您可以对函数使用析构,例如:

1
2
3
4
5
6
7
(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}

您可以使用clojure.walk。

1
2
3
4
5
6
7
8
9
10
11
(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}}

我不知道这样的功能是Clojure的一部分。 您可能必须自己编写。 我想出了这个:

1
2
3
4
5
6
7
8
9
10
11
(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))

要求路径是矢量,所以您可以使用peek(比last快得多)。 减少这样的路径:

1
2
3
4
(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}

当然,这假设您的所有终端键都是唯一的。