Search code examples
vectorclojurehashmap

Clojure; How to iterate through a vector of maps, so that the index is known, to be able to update-in the map?


Suppose i had this atom as the state of a game:

(defonce state (atom {:player
                       {:cells [{:x 123 :y 456 :radius: 1.7 :area 10}
                                {:x 456 :y 789 :radius: 1.7 :area 10}
                                {...}]}}))

And i wanted to read where one of the cells is, (the maps :x and :y values in the :cells vector) calculate with those values where they should be in the next frame and then when calculated update the new :x and :y positions in the respective map?

I have this so far:

(let [cells (get-in @state [:player :cells])]
  (reduce
    (fn
      [seq cell]
      (let [cell-x (get-in cell [:x])
            cell-y (get-in cell [:y])]
        ; do my calculations
        ; ...
        ))
    nil
    cells))

So i can read the values and do the calculation but how would i update the x and y position with the new values? I could use:

(swap! state update-in [:player :cells ...] assoc :x new-x :y new-y)

But there i don't know the index ... in which vector to update it in.

I assume there is a way without using reduce that would give me the index?

Or am i approaching this totally un-idiomatic?


Solution

  • You can update a particular hash-map object without knowing the index in the vector:

    (let [when-x 123
          new-x -1
          new-y -1]
     (swap! state update-in [:player :cells]
            (fn [v] (mapv (fn [{:keys [x y] :as m}]
                            (if (= x when-x)
                              (assoc m :x new-x :y new-y)
                              m))
                          v))))
    ;;=> {:player {:cells [{:x -1, :y -1, :radius 1.7, :area 10} 
    ;;                     {:x 456, :y 789, :radius 1.7, :area 10}]}}
    

    This code is useful where you have some criterion for which potentially many values need to be updated. Note that here we don't need to know the index to do the update.

    Going on a slight excursion now, but if you needed to update only a particular already known index then one way would be to use the same technique but with map-indexed instead of mapv:

    (let [when-idx 1
          new-x -1
          new-y -1]
      (swap! state update-in [:player :cells]
             (fn [v] (vec (map-indexed (fn [n {:keys [x y] :as m}]
                                         (if (= n when-idx)
                                           (assoc m :x new-x :y new-y)
                                           m))
                                       v)))))
    

    However that would be pointless with your data since a vector is an associative collection and thus update-in will be able to select by index:

    (let [when-idx 0
          new-x -1
          new-y -1]
       (swap! state update-in [:player :cells when-idx] #(assoc % :x new-x :y new-y)))
    

    Interestingly note that it would not however be pointless if instead of a vector you had a list, so '({:x 123 :y 456 :radius 1.7 :area 10}{:x 456 :y 789 :radius 1.7 :area 10}). With this non-associative collection you cannot use update-in.

    Another reason this construction would not be pointless is if you are worried about performance: you can use laziness to short-circuit finding the answer:

    (defn replace-in [v [idx new-val]]
      (concat (subvec v 0 idx)
              [new-val]
              (subvec v (inc idx))))
    
    (let [when-x 123
          new-x -1
          new-y -1]
      (swap! state update-in [:player :cells]
             (fn [v] (->> v
                          (keep-indexed (fn [idx {:keys [x] :as m}]
                                          (when (= x when-x) 
                                            [idx (assoc m :x new-x :y new-y)])))
                          first
                          (replace-in v)))))
    

    keep-indexed is similar to map-indexed, except that any nil values are not returned into the output sequence. Once the first value is realised the rest of the potential values are never generated, hence the short-circuit. Here idx is used by calls to subvec to chop up the original vector and include the new hash-map object.