Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

clojure way to update a map inside a vector

Tags:

clojure

What is the clojure way to update a map inside a vector e.g. if I have something like this, assuming each map has unique :name

(def some-vec
  [{:name "foo"
    ....}
   {:name "bar"
    ....}
   {:name "baz"
    ....}])

I want to update the map in someway if it has :name equal to foo. Currently I'm using map, like this

(map (fn [{:keys [name] :as value}]
       (if-not (= name "foo")
         value
         (do-something .....))) some-vec)

But this will loop through the entire vector even though I only update one item.

like image 232
Faris Nasution Avatar asked Sep 04 '14 11:09

Faris Nasution


4 Answers

Keep the data as a map instead of a vector of map-records, keyed by :name.

(def some-data
  {"foo" {:name "foo" :other :stuff}
   "bar" {:name "bar" :other :stuff}
   "baz" {:name "baz" :other :stuff}})

Then

(assoc-in some-data ["bar" :other] :things)

produces

{"foo" {:other :stuff, :name "foo"},
 "bar" {:other :things, :name "bar"},
 "baz" {:other :stuff, :name "baz"}}

in one go.

You can capture the basic manipulation in

(defn assoc-by-fn [data keyfn datum]
  (assoc data (keyfn datum) datum))

When, for example,

(assoc-by-fn some-data :name {:name "zip" :other :fassner})

produces

{"zip" {:other :fassner, :name "zip"},
 "foo" {:other :stuff, :name "foo"},
 "bar" {:other :stuff, :name "bar"},
 "baz" {:other :stuff, :name "baz"}}
like image 152
Thumbnail Avatar answered Oct 05 '22 23:10

Thumbnail


Given that you have a vector of maps, your code looks fine to me. Your concern about "looping through the entire vector" is a natural consequence of the fact that you're doing a linear search for the :name and the fact that vectors are immutable.

I wonder whether what you really want is a vector of maps? Why not a map of maps?

(def some-map
  {"foo" {...}
   "bar" (...}
   "baz" {...}}

Which you could then update with update-in?

like image 24
Paul Butcher Avatar answered Oct 05 '22 21:10

Paul Butcher


Given this shape of the input data and unless you have an index that can tell you which indices the maps with a given value of :name reside at, you will have to loop over the entire vector. You can, however, minimize the amount of work involved in producing the updated vector by only "updating" the matching maps, rather than rebuilding the entire vector:

(defn update-values-if
  "Assumes xs is a vector. Will update the values for which
  pred returns true."
  [xs pred f]
  (let [lim (count xs)]
    (loop [xs xs i 0]
      (if (< i lim)
        (let [x (nth xs i)]
          (recur (if (pred x)
                   (assoc xs i (f x))
                   xs)
                 (inc i)))
        xs))))

This will perform as many assoc operations as there are values in xs for which pred returns a truthy value.

Example:

(def some-vec [{:name "foo" :x 0} {:name "bar" :x 0} {:name "baz" :x 0}])

(update-values-if some-vec #(= "foo" (:name %)) #(update-in % [:x] inc))
;= [{:name "foo", :x 1} {:name "bar", :x 0} {:name "baz", :x 0}]

Of course if you're planning to transform the vector in this way with some regularity, then Thumbnail's and Paul's suggestion to use a map of maps will be a much more significant improvement. That remains the case if :name doesn't uniquely identify the maps – in that case, you could simply transform your original vector using frequencies and deal with a map of vectors (of maps with a given :name).

like image 44
Michał Marczyk Avatar answered Oct 05 '22 22:10

Michał Marczyk


If you're working with vector, you should know index of element that you want to change, otherwise you have to traverse it in some way.

I can propose this solution:

(defn my-update [coll val fnc & args]
  (let [index (->> (map-indexed vector coll)
                   (filter (fn [[_ {x :name}]] (= x val)))
                   ffirst)]
    (when index
      (apply update-in coll [index] fnc args))))

Where:
coll - given collection of maps; val - value of field :name; fnc - updating function; args - arguments of the updating function.

Let's try it:

user> (def some-vec
        [{:name "foo"}
         {:name "bar"}
         {:name "baz"}])
;; => #'user/some-vec
user> (my-update some-vec "foo" assoc :boo 12)
;; => [{:name "foo", :boo 12} {:name "bar"} {:name "baz"}]
user> (my-update some-vec "bar" assoc :wow "wow!")
;; => [{:name "foo"} {:name "bar", :wow "wow!"} {:name "baz"}]

I think that Thumbnail's answer may be quite useful for you. If you can keep your data as a map, these manipulations become much easier. Here is how you can transform your vector into a map:

user> (apply hash-map (interleave (map :name some-vec) some-vec))
;; => {"foo" {:name "foo"}, "bar" {:name "bar"}, "baz" {:name "baz"}}
like image 31
Mark Karpov Avatar answered Oct 05 '22 21:10

Mark Karpov