Search code examples
clojurespecter

Recursive map query using specter


Is there a simple way in specter to collect all the structure satisfying a predicate ?

(./pull '[com.rpl/specter "1.0.0"])

(use 'com.rpl.specter)

(def data {:items [{:name "Washing machine"
                    :subparts [{:name "Ballast" :weight 1}
                               {:name "Hull"    :weight 2}]}]})



(reduce + (select [(walker :weight) :weight] data))
;=> 3

(select [(walker :name) :name] data)
;=> ["Washing machine"]

How can we get all the value for :name, including ["Ballast" "Hull"] ?


Solution

  • Here's one way, using recursive-path and stay-then-continue to do the real work. (If you omit the final :name from the path argument to select, you'll get the full “item / part maps” rather than just the :name strings.)

    (def data
      {:items [{:name "Washing machine"
                :subparts [{:name "Ballast" :weight 1}
                           {:name "Hull" :weight 2}]}]})
    
    (specter/select
      [(specter/recursive-path [] p
         [(specter/walker :name) (specter/stay-then-continue [:subparts p])])
       :name]
      data)
    ;= ["Washing machine" "Ballast" "Hull"]
    

    Update: In answer to the comment below, here's a version of the above the descends into arbitrary branches of the tree, as opposed to only descending into the :subparts branch of any given node, excluding :name (which is the key whose values in the tree we want to extract and should not itself be viewed as a branching off point):

    (specter/select
      [(specter/recursive-path [] p
         [(specter/walker :name)
          (specter/stay-then-continue
            [(specter/filterer #(not= :name (key %)))
             (specter/walker :name)
             p])])
       :name]
      ;; adding the key `:subparts` with the value [{:name "Foo"}]
      ;; to the "Washing machine" map to exercise the new descent strategy
      (assoc-in data [:items 0 :subparts2] [{:name "Foo"}]))
    
    ;= ["Washing machine" "Ballast" "Hull" "Foo"]