How can I get the nested keys of a map in clojure?

后端 未结 11 1113
梦如初夏
梦如初夏 2020-12-15 10:59

if my structure is

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

I

相关标签:
11条回答
  • 2020-12-15 11:11

    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 doalling 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.)

    0 讨论(0)
  • 2020-12-15 11:18
    (defn keys-in [m]
      (if (or (not (map? m))
              (empty? m))
        '(())
        (for [[k v] m
              subkey (keys-in v)]
          (cons k subkey))))
    
    0 讨论(0)
  • 2020-12-15 11:18

    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]}
    
    0 讨论(0)
  • 2020-12-15 11:19

    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)
    
    0 讨论(0)
  • 2020-12-15 11:20

    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.

    0 讨论(0)
  • 2020-12-15 11:21

    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)))
    
    0 讨论(0)
提交回复
热议问题