Fork me on GitHub
#clojure
<
2022-10-30
>
agorgl12:10:48

Hello! What is the idiomatic clojure way of updating a map inside a vector (that may also live in another map)? Example:

{:cast "muppets"
 :characters
 [{:name "kermit"
   :address "foo"}
  {:name "gonzo"
   :address "faa"}
  {:name "pepe"
   :address "fii"}]}
I want to update the address for the vector item that has a :name that equals "gonzo" to sesame street and have the whole data structure updated like:
{:cast "muppets"
 :characters
 [{:name "kermit"
   :address "foo"}
  {:name "gonzo"
   :address "sesame street"}
  {:name "pepe"
   :address "fii"}]}

localshred12:10:16

You'll want to us a combination of update, assoc, and map

localshred12:10:19

(let [m {:cast "muppets"
        :characters
        [{:name    "kermit"
          :address "foo"}
         {:name    "gonzo"
          :address "faa"}
         {:name    "pepe"
          :address "fii"}]}
    m (update m :characters (fn [characters]
                                (map (fn [character]
                                        (if (= (:name character) "gonzo")
                                        (assoc character :address "sesame street")
                                        character)))))]
;; m now has the updated nested map
)

kwladyka12:10:18

the alternative is to no keep characters as

[{:name "kermit"
   :address "foo"}
  {:name "gonzo"
   :address "faa"}
  {:name "pepe"
   :address "fii"}]
but
{id {:name "kermit"
   :address "foo"}
id2   {:name "gonzo"
   :address "faa"}
}

localshred12:10:59

right, if your characters were a map keyed by name or id or whatever, it changes it quite a bit

kwladyka12:10:01

where id can be for example a :name

localshred12:10:11

(let [m {:cast "muppets"
           :characters
           {:kermit {:name    "kermit"
                     :address "foo"}
            :gonzo  {:name    "gonzo"
                     :address "faa"}
            :pepe   {:name    "pepe"
                     :address "fii"}}}
        m (update-in m [:characters :gonzo :address] "sesame street")]
;; m now has the updated nested map
)

localshred12:10:16

Much simpler, for sure

agorgl12:10:08

I'm using something in the lines of:

(defn- index-of [pred coll]
  (first (keep-indexed #(when (pred %2) %1) coll)))
.....
   (let [idx (index-of #(= (:name %) muppet) (get-in m [:characters]))]
     (assoc-in db [:characters idx :address] address))))

agorgl12:10:30

I just don't know if it idiomatic / good practice

kwladyka12:10:35

in my personal taste simplicity is always on the first place. Unless it has to be be really good performance, then it can be a little more complex, but not too much 😉

localshred12:10:38

others may have a different view, but I tend to arrange my data structures as maps if I need keyed access/update into them. You're traversing the data structure several times, first to get the index, and then to do the assoc-in. Obviously depending on the size of the structure that may be cumbersome

localshred12:10:32

though of course if your data is coming from some source in that format, you'll have to traverse it to make it accessible, so perhaps it's not as problematic?

kwladyka12:10:33

in other words answer yourself what is simpler and more readable for you

🙌 1
agorgl12:10:02

Thanks for the suggestions guys

localshred12:10:34

(defn group-by-key
    [k ms]
    (reduce (fn [acc m] (assoc acc (get m k) m))
            {}
            ms))

  (group-by-key :name [{:name :foo}
                       {:name :bar}])
  ;; => {:foo {:name :foo}, :bar {:name :bar}}

  (def local-data (atom (-> data-from-source
                            (update :characters (partial group-by-key :name)))))

  (def update-character
    [name address]
    (swap! local-data
           (fn [data]
             (update-in data
                        [:characters name :address]
                        address)))

skylize18:10:51

Code below not tested: If you want to use the index, you can at least take advantage to short circuit on a match.

(defn index-of [pred coll]
  (some (fn [i] (and (pred (coll i)) i)) (range (count coll))))
If you are okay with not short circuiting, I think this can be much simpler by mapping a conditional transformation over the whole vector.
(update m :characters
  #(map (fn [c]
                  (if (pred c) (transform c) c))
               %))

mdiin08:10:44

If you don’t mind bringing in Specter as a dependency, you could do something like this: (Specter/transform [:characters ALL (pred #(= (:name %) "gonzo"))] #(assoc % :address "Sesame Street") <data>). This is totally untested, so might even be wrong, but Specter is built to handle this sort of nested transforms and selects.

tgg21:10:35

Anyone remember the name of the library that converts jetty errors into something much nicer? Can’t remember nor work out how to google for it!

👀 1
tgg21:10:17

Maybe it used to be a part of luminus… I can check there duckie

tgg21:10:35

Maybe not rich2

kwladyka22:10:15

Did something change for org.clojure/tools.logging {:mvn/version "1.2.4"} and -Dclojure.tools.logging.factory=clojure.tools.logging.impl/jul-factory? I use Java 11. Whenever I log something I always see clojure.tools.logging$eval3401$fn__3404 invoke which wasn’t there some time ago. It started to appear in all projects on my computer. Whenever it is builded to jar or REPL. The same code still work as expected in google cloud, but not on my computer. I miss something obvious or I have some kind of dementia ;) (l/info "foo")

Oct 30, 2022 10:19:24 PM clojure.tools.logging$eval3401$fn__3404 invoke
INFO: foo

hiredman00:10:49

That is the default log format that java.util.logging produces

hiredman00:10:28

time class method level message

kwladyka09:10:30

but it shouldn’t be clojure.tools.logging$eval3401$fn__3404, but the ns$fn from my code right?