if my structure is
{ :a :A
:b :B
:c {
:d :D
}
:e {
:f {
:g :G
:h :H
}
}
}
I
Here are solutions (without intermediate paths) using Specter. They're by Nathan Marz, Specter's author, from a conversation on the Specter Slack channel (with his permission). I claim no credit for these definitions.
Simple version:
(defn keys-in [m]
(let [nav (recursive-path [] p
(if-path map?
[ALL (collect-one FIRST) LAST p]
STAY))]
(map butlast (select nav m))))
More efficient version:
(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)))
My informal explanation of what's happening in these definitions:
In the simple version, since the top-level argument is a map, if-path map?
passes it to the first collection of navigators in brackets. These begin with ALL
, which says here to do the rest for each element in the map. Then for each MapEntry
in the map, (collect-one FIRST)
says to add its first element (key) to the result of passing its last element (val) to if-path
again. p
was bound by recursive-path
to be a reference to that same recursive-path
expression. By this process we eventually get to a non-map. Return it and stop processing on that branch; that's what STAY
means. However, this last thing returned is not one of the keys; it's the terminal val. So we end up with the leaf vals in each sequence. To strip them out, map butlast
over the entire result.
The second version avoids this last step by only recursing into the val in the MapEntry
if that val is itself a map. That's what the inner if-path
does: [LAST map?]
gets the last element, i.e. the val of the current MapEntry
generated by ALL
, and passes it to map?
.
I used Criterium to test all of the key path functions on this page that don't return intermediate paths, plus one by noisesmith that's part of an answer to another question. For a 3-level, 3 keys per level map and for a 6-level, 6 keys per level map, miner49r's version and the second, faster Specter version have similar speeds, and are much faster than any of the other versions.
Timings on a 3-level, 3 keys per level (27 paths) map, in order:
vec
): 243.756275 µsTimings on a 6-level, 6 keys per level (6^6 = 46656 paths) map, in order:
vec
): 839.266448 msAll calls were wrapped in doall
so that lazy results would be realized. Since I was doall
ing them, I took out vec
wrapper in Alex Miller's definition.
Full details about timings can be found here. The test code is here.
(The simple Specter version is slower than the faster version because of the use of map butlast
to strip out the leaf values. If this is step is removed, the simple Specter definition's times are similar to those of the second definition.)
(defn keys-in [m]
(if (or (not (map? m))
(empty? m))
'(())
(for [[k v] m
subkey (keys-in v)]
(cons k subkey))))
Got a similar question, wasn't satisfied by current solutions:
"Naive" recursive approach
(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]}
You can build this with clojure.zip or tree-seq fairly easily though I strongly prefer the prismatic.schema library for verifying the structure of nested maps
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)
Here is a generic solution for known collection types, including maps (look for "Key Paths" on the Readme page for usage examples).
It handles mixed types as well (sequential types, maps and sets), and the API (protocols) can be extended to other types.
This answer of mine is just to illustrate how NOT to do it since it is still procedural.
(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)))