uli
uli

Reputation: 31

Using clojure, Is there a better way to to remove a item from a sequence, which is the value in a map?

There is a map containing sequences. The sequences contain items. I want to remove a given item from any sequence that contains it.

The solution I found does what it should, but I wonder if there is a better or more elegant way to achieve the same.

my current solution:

(defn remove-item-from-map-value [my-map item]
    (apply merge (for [[k v] my-map] {k (remove #(= item %) v)})))

The test describe the expected behaviour:

(require '[clojure.test :as t])

(def my-map {:keyOne   ["itemOne"]
             :keyTwo   ["itemTwo" "itemThree"]
             :keyThree ["itemFour" "itemFive" "itemSix"]})

(defn remove-item-from-map-value [my-map item]
  (apply merge (for [[k v] my-map] {k (remove #(= item %) v)})))

(t/is (= (remove-item-from-map-value my-map "unknown-item") my-map))
(t/is (= (remove-item-from-map-value my-map "itemFive") {:keyOne   ["itemOne"]
                                                         :keyTwo   ["itemTwo" "itemThree"]
                                                         :keyThree ["itemFour" "itemSix"]}))

(t/is (= (remove-item-from-map-value my-map "itemThree") {:keyOne   ["itemOne"]
                                                          :keyTwo   ["itemTwo"]
                                                          :keyThree ["itemFour" "itemFive" "itemSix"]}))

(t/is (= (remove-item-from-map-value my-map "itemOne") {:keyOne   []
                                                        :keyTwo   ["itemTwo" "itemThree"]
                                                        :keyThree ["itemFour" "itemFive" "itemSix"]}))

I'm fairly new to clojure and am interested in different solutions. So any input is welcome.

Upvotes: 2

Views: 427

Answers (5)

JovanToroman
JovanToroman

Reputation: 705

Here's a one-liner which does this in an elegant way. The perfect function for me to use in this scenario is clojure.walk/prewalk. What this fn does is it traverse all of the sub-forms of the form that you pass to it and it transforms them with the provided fn:

(defn remove-item-from-map-value [data item] 
  (clojure.walk/prewalk #(if (map-entry? %) [(first %) (remove #{item} (second %))] %) data))

What the remove-item-from-map-value fn will do is it will check if current form is a map entry and if so, it will remove specified key from its value (second element of the map entry, which is a vector containing a key and a value, respectively).

The best this about this approach is that is is completely extendable: you could decide to do different things for different types of forms, you can also handle nested forms, etc.

It took me some time to master this fn but once I got it I found it extremely useful!

Upvotes: 0

leetwinski
leetwinski

Reputation: 17859

i would go with something like this:

user> (defn remove-item [my-map item]
        (into {}
              (map (fn [[k v]] [k (remove #{item} v)]))
              my-map))
#'user/remove-item

user> (remove-item my-map "itemFour")

;;=> {:keyOne ("itemOne"),
;;    :keyTwo ("itemTwo" "itemThree"),
;;    :keyThree ("itemFive" "itemSix")}

you could also make up a handy function map-val performing mapping on map values:

(defn map-val [f data]
  (reduce-kv
   (fn [acc k v] (assoc acc k (f v)))
   {} data))

or shortly like this:

(defn map-val [f data]
  (reduce #(update % %2 f) data (keys data)))

user> (map-val inc {:a 1 :b 2})
;;=> {:a 2, :b 3}

(defn remove-item [my-map item]
  (map-val (partial remove #{item}) my-map))

user> (remove-item my-map "itemFour")
;;=> {:keyOne ("itemOne"),
;;    :keyTwo ("itemTwo" "itemThree"),
;;    :keyThree ("itemFive" "itemSix")}

Upvotes: 4

alex314159
alex314159

Reputation: 3247

Another solution much like @leetwinski:

(defn remove-item [m i]
  (zipmap (keys m)
          (map (fn [v] (remove #(= % i) v))
               (vals m)))) 

Upvotes: 0

cfrick
cfrick

Reputation: 37033

I throw in the specter version for good measure. It keeps the vectors inside the map and it's really compact.

(setval [MAP-VALS ALL #{"itemFive"}] NONE my-map)

Example

user=> (use 'com.rpl.specter)
nil
user=> (def my-map {:keyOne   ["itemOne"]
  #_=>              :keyTwo   ["itemTwo" "itemThree"]
  #_=>              :keyThree ["itemFour" "itemFive" "itemSix"]})
  #_=> 
#'user/my-map
user=> (setval [MAP-VALS ALL #{"itemFive"}] NONE my-map)
{:keyOne ["itemOne"],
 :keyThree ["itemFour" "itemSix"],
 :keyTwo ["itemTwo" "itemThree"]}
user=> (setval [MAP-VALS ALL #{"unknown"}] NONE my-map)
{:keyOne ["itemOne"],
 :keyThree ["itemFour" "itemFive" "itemSix"],
 :keyTwo ["itemTwo" "itemThree"]}

Upvotes: 4

schaueho
schaueho

Reputation: 3504

I think your solution is mostly okay, but I would try to avoid the apply merge part, as you can easily recreate a map from a sequence with into. Also, you could also use map instead of for which I think is a little bit more idiomatic in this case as you don't use any of the list comprehension features of for.

(defn remove-item-from-map-value [m item]
    (->> m
         (map (fn [[k vs]]
                {k (remove #(= item %) vs)}))
         (into {})))

Upvotes: 1

Related Questions