如何在clojure中获取地图的嵌套键?

时间:2014-02-14 00:51:03

标签: dictionary recursion clojure functional-programming

如果我的结构是

{ :a :A
  :b :B
  :c {
       :d :D
     }
  :e {
       :f {
            :g :G
            :h :H
          }
     }
}

我想获得一个名为keys-in的函数,它返回类似于:

的函数
[[:a] [:b] [:c :d] [:e :f :g] [:e :f :h]]

所以我可以这样做:

(not-any? nil? (map #(get-in my-other-map %1) (keys-in my-map)))

因此,我可以确定my-other-map具有与my-map

相同的键

11 个答案:

答案 0 :(得分:13)

(defn keys-in [m]
  (if (map? m)
    (vec 
     (mapcat (fn [[k v]]
               (let [sub (keys-in v)
                     nested (map #(into [k] %) (filter (comp not empty?) sub))]
                 (if (seq nested)
                   nested
                   [[k]])))
             m))
    []))

;; tests
user=> (keys-in nil)
[]
user=> (keys-in {})
[]
user=> (keys-in {:a 1 :b 2}))
[[:a] [:b]]
user=> (keys-in {:a {:b {:c 1}}})
[[:a :b :c]]
user=> (keys-in {:a {:b {:c 1}} :d {:e {:f 2}}})
[[:a :b :c] [:d :e :f]]

答案 1 :(得分:8)

(defn keys-in [m]
  (if (or (not (map? m))
          (empty? m))
    '(())
    (for [[k v] m
          subkey (keys-in v)]
      (cons k subkey))))

答案 2 :(得分:4)

强制性拉链版

(require '[clojure.zip :as z])

(defn keys-in [m] 
  (letfn [(branch? [[path m]] (map? m)) 
          (children [[path m]] (for [[k v] m] [(conj path k) v]))] 
    (if (empty? m) 
      []
      (loop [t (z/zipper branch? children nil [[] m]), paths []] 
        (cond (z/end? t) paths 
              (z/branch? t) (recur (z/next t), paths) 
              :leaf (recur (z/next t), (conj paths (first (z/node t)))))))))

答案 3 :(得分:4)

如果您不需要懒惰的结果而只想快速,请尝试使用(defn keypaths ([m] (keypaths [] m ())) ([prev m result] (reduce-kv (fn [res k v] (if (map? v) (keypaths (conj prev k) v res) (conj res (conj prev k)))) result m)))

get-in

如果您还想支持向量索引(与update-inassociative?一样),请使用map?代替(defn kvpaths-all2 ([m] (kvpaths-all2 [] m ())) ([prev m result] (reduce-kv (fn [res k v] (if (associative? v) (let [kp (conj prev k)] (kvpaths-all2 kp v (conj res kp))) (conj res (conj prev k)))) result m))) 进行测试。如果你想要中间路径,你也可以联合起来。这是一个变种:

{{1}}

答案 4 :(得分:2)

你可以很容易地用clojure.zip或tree-seq构建它,虽然我更喜欢prismatic.schema库来验证嵌套映射的结构

user> (def my-data-format                                 
  {:a Keyword                                             
   :b Keyword                                             
   :c {:d Keyword}                                        
   :e {:f {:g Keyword                                     
           :h Keyword}}})                                 
#'user/my-data-format                                     
user> (def some-data                                      
         {:a :A                                            
          :b :B                                            
          :c {:d :D}                                       
          :e {:f {:g :G                                    
                  :h :G}}})                                
#'user/some-data                                          
user> (schema/validate my-data-format some-data)          
{:a :A, :c {:d :D}, :b :B, :e {:f {:g :G, :h :G}}}
user> (def some-wrong-data
        {:a :A
         :b :B
         :c {:wrong :D}
         :e {:f {:g :G
                 :h :G}}})
 #'user/some-wrong-data             

 user> (schema/validate my-data-format some-wrong-data)  

ExceptionInfo Value does not match schema: 
{:c {:d missing-required-key, 
     :wrong disallowed-key}}  
schema.core/validate (core.clj:132)

答案 5 :(得分:2)

得到了类似的问题,目前的解决方案并不满意:

“天真”的递归方法

(require '[clojure.set :as set])

(defn all-paths
  ([m current]
   ;; base case: map empty or not a map
   (if (or (not (map? m)) (empty? m))
     #{current}
   ;; else: recursive call for every (key, value) in the map
     (apply set/union #{current}
            (map (fn [[k v]]
                   (all-paths v (conj current k)))
                 m))))
  ([m]
   (-> m (all-paths []) (disj []))))


(all-paths {:a 1
            :b 2
            :c {:ca 3
                :cb {:cba 4
                     :cbb 5}}
            :d {:da 6
                :db 7}})
=> #{[:a] [:b] [:c] [:d] [:c :ca] [:c :cb] [:d :da] [:d :db] [:c :cb :cba] [:c :cb :cbb]}

答案 6 :(得分:2)

以下是使用Specter的解决方案(没有中间路径)。他们是Spectre的作者Nathan Marz,来自Spectre Slack频道的conversation(获得他的许可)。我对这些定义不予置评。

简单版本:

(defn keys-in [m]
  (let [nav (recursive-path [] p
              (if-path map?
                [ALL (collect-one FIRST) LAST p]
                STAY))]
    (map butlast (select nav m))))

更高效的版本:

(defn keys-in [m]
  (let [nav (recursive-path [] p
              (if-path map?
                [ALL
                 (if-path [LAST map?]
                  [(collect-one FIRST) LAST p]
                  FIRST)]))]
    (select nav m)))

我对这些定义中发生的事情的非正式解释:

在简单版本中,由于顶级参数是地图,if-path map?会将其传递给括号中的第一个导航器集合。这些以ALL开头,这里说这是为地图中的每个元素做的其余部分。然后,对于地图中的每个MapEntry(collect-one FIRST)表示将其第一个元素(键)添加到将其最后一个元素(val)再次传递给if-path的结果中。 p绑定recursive-path作为对同一recursive-path表达式的引用。通过这个过程,我们最终得到一个非地图。返回并停止对该分支的处理;那是STAY的意思。但是,最后返回的东西不是其中一个键;这是终端val。所以我们最终得到了每个序列中的叶子。要删除它们,请在整个结果上映射butlast

第二个版本通过仅递归到MapEntry中的val来避免这最后一步,如果该val本身就是一个映射。这就是内部if-path所做的事情:[LAST map?]获取最后一个元素,即MapEntry生成的当前ALL的val,并将其传递给map?

我使用Criterium来测试这个页面上不返回中间路径的所有关键路径函数,加上一个由answer to another question部分的noisesmith测试。对于3级,3级每层级映射和6级,6级密钥/级别映射,miner49r的版本和第二个更快的Spectre版本具有相似的速度,并且比任何其他版本快得多。

按照以下顺序在3级,3级/ 3级(27路径)地图上进行计时:

  • miner49r's:29.235649μs
  • ñ。 Marz的第二个幽灵:30.590085μs
  • ñ。 Marz的第一个幽灵:62.840230μs
  • amalloy's:75.740468μs
  • noisesmith(来自其他问题):87.693425μs
  • AWebb:162.281035μs
  • AlexMiller(不含vec):243.756275μs

按照以下顺序在6级,每级6个键(6 ^ 6 = 46656路径)上进行计时:

  • ñ。 Marz的第二个幽灵:34.435956 ms
  • miner49r's:37.897345 ms
  • ñ。 Marz的第一个幽灵:119.600975 ms
  • noisesmith's:180.448860 ms
  • amalloy's:191.718783 ms
  • AWebb's:193.172784 ms
  • AlexMiller(不含vec):839.266448 ms

所有调用都包含在doall中,以便实现惰性结果。由于我doall,我在Alex Miller的定义中取出vec包装。 有关时间安排的详细信息,请参见here。测试代码为here

(简单的Specter版本比速度更快的版本慢,因为使用map butlast来去除叶子值。如果删除了这个步骤,那么简单的Spectre定义的时间与第二个类似定义。)

答案 7 :(得分:1)

我的这个答案只是为了说明如何不这样做,因为它仍然是程序性的。

(defn keys-in [data] (genkeys [] data))

(defn genkeys [parent data]
  (let [mylist (transient [])]
    (doseq [k (keys data)]
      (do
        (if ( = (class (k data)) clojure.lang.PersistentHashMap )
          (#(reduce conj! %1 %2) mylist (genkeys (conj parent  k ) (k data) ))
          (conj! mylist  (conj parent  k ) )
          )))
    (persistent! mylist)))

答案 8 :(得分:1)

这是一个基于lazy-seq返回所有键(不仅仅是终端键)的实现:

(defn keys-in
  ([m] (if (map? m) (keys-in (seq m) [])))
  ([es c]
   (lazy-seq
    (when-let [e (first es)]
      (let [c* (conj c (key e))]
        (cons c* (concat (if (map? (val e)) (keys-in (seq (val e)) c*))
                         (keys-in (rest es) c))))))))

答案 9 :(得分:0)

为个人项目开展类似工作,这是我天真的实施:

(defn keys-in
  [m parent-keys]
  (mapcat (fn [[k v]]
        (if (map? v)
          (keys-in v (conj parent-keys k))
          (vector (conj parent-keys k v))))
      m))

从repl:

中使用它
(keys-in <your-map> [])

奇特的方式:

(map (comp vec drop-last) (keys-in <your-map> []))

答案 10 :(得分:0)

Here是已知集合类型(包括地图)的通用解决方案(在自述页面上查找“关键路径”以获取用法示例)。

它还处理混合类型(顺序类型,映射和集合),并且API(协议)可以扩展为其他类型。