2

Supposing I has a data structure like this:

[[{:name "bob" :favorite-color "green"}{:name "tim" :favorite-color "blue"}]
[{:name "eric" :favorite-color "orange"}{:name "jim" :favorite-color "purple"}]
[{:name "andy" :favorite-color "green"}{:name "tom" :favorite-color "blue"}]]

and an array like this:

["green" "purple"]

How would I pass over my data structure and augment all maps for folks who liked the colors in my array with a new key value pair of :likes-my-colors "yes" ?

The result would be:

[[{:name "bob" :favorite-color "green" :likes-my-colors "yes"}{:name "tim" :favorite-color "blue"}]
 [{:name "eric" :favorite-color "orange"}{:name "jim" :favorite-color "purple" :likes-my-colors "yes"}]
 [{:name "andy" :favorite-color "green" :likes-my-colors "yes"}{:name "tom" :favorite-color "blue"}]]

(I intentionally made the value a string of yes as opposed to true because that's closer to what I am trying to figure out).

I tried loop and recur with postwalk but couldn't figure out how to mutate the map with subsequent recursions. I won't paste my horrid attempt here because I am guessing there's a better way to do it then with recur. However, postwalk would have the advantage of being able to handle more an increasingly nested data structure, which will likely be the case. So maybe recur with postwalk is the way to go.

I'm using ClojureScript and Reagent to store app state in an atom... as things occur I need to keep updating the app state in that atom. The app state gets reset repeatedly in a single user session... it gets built up and modified after each reset. As in this example, the app state gets modified based on arrays. My code needs to work through the elements of the array and modify all the maps that meet a condition. Eventually, this structure is used to add classes to a Hiccup data structure. The UI changes accordingly; people in a list would have borders appear around them if they liked my colors, for example, by having a class added.

I had awesome help in learning how to look through a data structure like this and update all maps given a specific key/value pair... but I've run into trouble doing it with a series of values. In other words, 'build up' a map in a sense... but it's more 'modify with multiple passes'. That phrasing will hopefully improve as my understanding does.

I am wondering, as a side note, how Clojure users go about accessing and mutating elements buried deeply in nested data structures. I'd rather have more complicated data structures but I avoid them because it seems hard to modify deep elements. I'm suspecting they might use libraries. It seems like there may be an easier way of getting at and modifying complex structures than writing brain teaser (for me) code. But then again, I may be wrong. There are a lot of examples online but they are often about modifying simple structures.

3
  • 1
    Is the nesting random or just list of list of maps like in your example? Walking usually only is needed if there is arbitrary nesting. Here a map or specter would do. Commented Apr 15, 2021 at 8:45
  • There will be arbitrary nesting. But I did take a look at specter... though it was a bit hard to figure out how to apply it here (though I could and should spend more time and effort looking at it). At the same time, the arbitrary nesting might be a result of my poor structuring and modeling of the data. Commented Apr 15, 2021 at 9:03
  • Your point about using walkers echoes leetwinski's caution about them below. Commented Apr 15, 2021 at 9:12

2 Answers 2

5

i would start with an item updater, like this for example:

(defn handle-fav-colors [color-set data]
    (if (color-set (:favorite-color data))
      (assoc data :likes-my-color "yes")
      data))

then you would be free to update your data any way you like. Like mapping:

user> (mapv (partial mapv (partial handle-fav-colors #{"green" "purple"})) data)

;;=> [[{:name "bob", :favorite-color "green", :likes-my-color "yes"}
;;     {:name "tim", :favorite-color "blue"}]
;;    [{:name "eric", :favorite-color "orange"}
;;     {:name "jim", :favorite-color "purple", :likes-my-color "yes"}]
;;    [{:name "andy", :favorite-color "green", :likes-my-color "yes"}
;;     {:name "tom", :favorite-color "blue"}]]

i won't personally recommend using walkers for this, since this one has a regular structure, while walkers go indefinitely deep, leading to a non-zero possibility to mess up some deeply nested maps. The rule of thumb (works for me): when you know the exact level in data structure you need to operate at, you should not use tools possibly operating above or below this level.

Also, as clojure (and FP in general) is all about small composable and reusable utils, you could approach with first making up the proper general functions like nested collections mapping:

(defn mapv-deep [level f data]
  (if (pos? level)
    (mapv (partial mapv-deep (dec level) f) data)
    (mapv f data)))

user> (mapv-deep 0 inc [1 2 3])
;; [2 3 4]
user> (mapv-deep 1 inc [[1 2] [3 4]])
;; [[2 3] [4 5]]
user> (mapv-deep 2 inc [[[1 2] [3 4]] [[5 6] [7 8]]])
;; [[[2 3] [4 5]] [[6 7] [8 9]]]

and conditional analog of assoc

(defn assoc-when [data pred k v]
  (if (pred data)
    (assoc data k v)
    data))

user> (assoc-when {:a 10 :b 20} #(-> % :a even?) :even-a? true)
;;=> {:a 10, :b 20, :even-a? true}

user> (assoc-when {:a 11 :b 20} #(-> % :a even?) :even-a? true)
;;=> {:a 11, :b 20}

so now the task can be solved this way:

(defn handle-fav-colors [color-set data]
  (assoc-when data (comp color-set :favorite-color) :likes-my-color "yes"))

user> (mapv-deep 1 (partial handle-fav-colors #{"green" "purple"}) data)

;;=> [[{:name "bob", :favorite-color "green", :likes-my-color "yes"}
;;     {:name "tim", :favorite-color "blue"}]
;;    [{:name "eric", :favorite-color "orange"}
;;     {:name "jim", :favorite-color "purple", :likes-my-color "yes"}]
;;    [{:name "andy", :favorite-color "green", :likes-my-color "yes"}
;;     {:name "tom", :favorite-color "blue"}]]
Sign up to request clarification or add additional context in comments.

3 Comments

This is amazing... just posted my question, and you all have responded so quickly. I was really frustrated. I am quite moved by all you helping. I am a bit tired from trying to figure this out for hours, but I'll come back soon and take a crack at it with these approaches.
Great point about using walkers... I can imagine really messing up something with them.
This is really great... how you walked me through a solution (haha see what I did there?) in terms of how I would reason and decompose it. Teach a person to fish. I am beginning to 'get' the functional mindset in terms of this decomposition/one-function-at-a-time and compose thing.
2

Lets write an utility function first for handling a single map. It will check if the value under :favorite-color is present in favorite-colors. Since favorite-colors is a vector, we need to convert it to a set so we can use contains? on it.

(defn handle-map [m]
  (if (contains? (set favorite-colors) (:favorite-color m))
     (assoc m :likes-my-colors "yes")
     m))

Now we can use postwalk to call it on all map nodes:

(clojure.walk/postwalk
   (fn [m]
     (if (map? m)
       (handle-map m)
       m))
   data)

2 Comments

Awesome, again! See my comment to leetwinski about how grateful I am to all of you. I really didn't know how to ask for help for quite a while with functional programming... or even how to structure the questions (although I still need quite a bit of work there!). As mentioned below, I'll take a break and come back with fresh eyes and try these great suggestions.
This did the trick! I think I should also understand and try the other suggestions due to the legitimate concerns expressed about my just throwing walkers at everything.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.