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:
- miner49r's: 29.235649 µs
- N. Marz's second Specter: 30.590085 µs
- N. Marz's first Specter: 62.840230 µs
- amalloy's: 75.740468 µs
- noisesmith's (from the other question): 87.693425 µs
- AWebb's: 162.281035 µs
- AlexMiller's (without
vec
): 243.756275 µs
Timings on a 6-level, 6 keys per level (6^6 = 46656 paths) map, in order:
- N. Marz's second Specter: 34.435956 ms
- miner49r's: 37.897345 ms
- N. Marz's first Specter: 119.600975 ms
- noisesmith's: 180.448860 ms
- amalloy's: 191.718783 ms
- AWebb's: 193.172784 ms
- AlexMiller's (without
vec
): 839.266448 ms
All 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.)