How to filter a persistent map in Clojure?

15,427

Solution 1

And another one:

(let [m {:a 1 :b 2 :c 1}]
  (select-keys m (for [[k v] m :when (= v 1)] k)))

Solution 2

(into {} (filter #(-> % val (= 1)) {:a 1 :b 1 :c 2}))

Of course this does rebuild the map from a sequence of map entries, but there is no way around it. If you're going to filter the entries by value, you're going to have to go through them one by one to see which values match your predicate and which don't.

Updated (see comments below):

With the newly introduced keep function, the source of which you can see here (should work just fine in Clojure 1.1 if you want to backport), this seems like a nice way to go about it if you don't use nil as a key:

(let [m {:a 1 :b 1 :c 2}]
  (apply dissoc m (keep #(-> % val (= 1) (if nil (key %))) m)))
; => {:a 1, :b 1}

Also, if you do actually see a slowdown related to rebuilding your map, you can use a transient map at the rebuilding step:

(persistent! (loop [m (transient {})
                    to-go (seq [[:a 1] [:b 2]])]
               (if to-go
                 (recur (apply assoc! m (first to-go))
                        (next to-go))
                 m)))
; => {:a 1, :b 2}

Solution 3

Need to traverse all entries, but can leverage Clojures persistent maps:

(apply dissoc my-map (for [[k v] my-map :when (not= v 1)] k))

Solution 4

Per your comment to Michał Marczyk:

(defn filter* [f map]
  (reduce (fn [m [k v :as x]]
            (if-not (f x)
              (dissoc m k)
              m))
          map map))

user> (filter* #(-> % val (= 1)) {:a 1 :b 1 :c 2})
{:a 1, :b 1}

I don't see that you're going to gain much with this vs. Michał's version.

Solution 5

Here's another one using reduce-kv

(defn filter-kv [pred map]
  (reduce-kv (fn [accumulator key value]
               (if (pred key value)
                 (assoc accumulator key value)
                 accumulator)) {} map))

Usage

(filter-kv (fn [key _]
             (not (= key "a"))) {"a" {:some "a"}
                                 "b" {:some "b"}
                                 "c" {:some "c"}})

>> {"b" {:some "b"}
    "c" {:some "c"}}
Share:
15,427
Alex B
Author by

Alex B

Refactoring extraordinaire.

Updated on July 10, 2022

Comments

  • Alex B
    Alex B almost 2 years

    I have a persistent map which I want to filter. Something like this:

    (filter #(-> % val (= 1)) {:a 1 :b 1 :c 2})
    

    The above comes out as ([:a 1] [:b 1]) (a lazy sequence of map entries). However I want to be get {:a 1 :b 1}.

    How can I filter a map so it remains a map without having to rebuild it from a sequence of map entries?

  • Alex B
    Alex B almost 14 years
    Well, theoretically you could have it filtered by value without rebuilding by returning a map with dissoc-ed keys that correspond to non-matching values. I was hoping there is a language-supported way to do it.
  • Michał Marczyk
    Michał Marczyk almost 14 years
    Ok, I see what you mean. I'll add two ways to do it in a second, but note that you're not very likely to see a great gain in the performance department (unless you've got a really huge map and you'll only be dissocing a small number of keys).
  • Michał Marczyk
    Michał Marczyk almost 14 years
    Hm, actually it's not so much "two ways to do it" as "one way to do it and one way to worry less about rebuilding". Not that you're very likely to need to worry. :-)
  • gtrak
    gtrak over 11 years
    into uses transient conj!, so I'm not sure there's a point in doing loop-recur
  • Dimagog
    Dimagog over 2 years
    You can change {} at the end with (empty map) to make it more generic and work for any collection supporting kv-reduce protocol.
  • Dimagog
    Dimagog over 2 years
    Also, this approach is more efficient when most keys will be removed, i.e. there will be a few assoc's into a result map that starts with empty {}. If there is an expectation that only a few keys will be removed, then it would be more efficient to start accumulator with the original map and reverse if to dissoc the filtered out keys.