[英]How can I get the nested keys of a map in clojure?
如果我的结构是
{ :a :A
:b :B
:c {
:d :D
}
:e {
:f {
:g :G
:h :H
}
}
}
我想获得一个名为keys-in
的函数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
具有相同的键
(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]]
(defn keys-in [m]
(if (or (not (map? m))
(empty? m))
'(())
(for [[k v] m
subkey (keys-in v)]
(cons k subkey))))
强制拉链版本
(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)))))))))
如果您不需要懒惰的结果而只想快速,请尝试使用reduce-kv
。
(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-in
),请使用associative?
测试associative?
而不是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)))
您可以使用 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)
有一个类似的问题,对当前的解决方案不满意:
“朴素”的递归方法
(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]}
以下是使用Spectre 的解决方案(没有中间路径)。 它们由 Spectre 的作者 Nathan Marz 撰写,来自 Spectre Slack 频道上的一次对话(经他许可)。 我认为这些定义没有功劳。
简单版:
(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)
说将其第一个元素 (key) 添加到将其最后一个元素 (val) 再次传递给if-path
。 p
被recursive-path
绑定为对相同recursive-path
表达式的引用。 通过这个过程,我们最终得到了一个非地图。 返回并停止在该分支上处理; 这就是STAY
意思。 但是,返回的最后一件事不是关键之一; 这是终端 val。 所以我们最终得到了每个序列中的叶值。 要butlast
它们, butlast
在整个结果上映射butlast
。
如果该 val 本身是一个映射,则第二个版本通过仅递归到MapEntry
中的 val 来避免这最后一步。 这就是内部if-path
所做的: [LAST map?]
获取最后一个元素,即ALL
生成的当前MapEntry
的 val,并将其传递给map?
.
我使用 Criterium 来测试此页面上所有不返回中间路径的关键路径函数,加上一个由 Noisesmith 提供的,它是另一个问题的答案的一部分。 对于 3 级、每级 3 个键的地图和 6 级、每级 6 个键的地图,miner49r 的版本和第二个更快的 Spectre 版本具有相似的速度,并且比任何其他版本都快得多。
3 级、每级 3 个键(27 条路径)地图上的时间,按顺序:
vec
):243.756275 µs6 级、每级 6 个键(6^6 = 46656 条路径)映射的时序,按顺序:
vec
):839.266448 毫秒所有调用都包含在doall
以便实现懒惰的结果。 由于我doall
荷兰国际集团他们,我拿出vec
在亚历克斯·米勒的定义包装。 可以在此处找到有关计时的完整详细信息。 测试代码在这里。
(简单的 Spectre 版本比更快的版本慢,因为使用map butlast
叶值。如果删除这一步,简单的 Spectre 定义的时间与第二个定义的时间相似。)
我的这个答案只是为了说明如何不这样做,因为它仍然是程序性的。
(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)))
这是一个基于惰性序列返回所有键(不仅仅是终端键)的实现:
(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))))))))
为个人项目做类似的事情,这是我天真的实现:
(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> []))
这是已知集合类型的通用解决方案,包括映射(在自述文件页面上查找“关键路径”以获取使用示例)。
它也处理混合类型(顺序类型、映射和集合),并且 API(协议)可以扩展到其他类型。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.