THX1137
THX1137

Reputation: 973

How do I find then update values in maps in a series of vectors in Clojure?

What would be an elegant way to find the ba4 values for :foo in a data structure like:

[[{:foo "ba1"}{:foo "ba2"}] [{:foo "ba3"}{:foo "ba4"}]]

and add the :bif "baf" key and value so the map(s) containing the key/value I sought so I had:

[[{:foo "ba1"}{:foo "ba2"}] [{:foo "ba3"}{:foo "ba4" :bif "baf"}]]?

This is the main thing I am trying to figure out; I do wonder about how you would do this if there were multiple levels of nested maps, but that will be next for me to understand.

I have, I think, seen various libraries out there for tackling this sort of thing and I'm not at all against using them, though they would have to work in ClojureScript.

I am trying to update a Reagent atom such that classes will change on a UI element.

Upvotes: 0

Views: 182

Answers (2)

Aaron Bell
Aaron Bell

Reputation: 862

(I learned a bit about decorators and merge from Rulle's solution.)

What you're describing is walking a nested data structure. Postwalking or prewalking take every possible subform of your nested data structure and applies a function against it.

Here's how I got your desired solution using postwalk (note: assoc adds a key-value to a map):

(ns walking.core
  (:require [clojure.walk :as w]))

(clojure.walk/postwalk #(if (and (map? %) (= (:foo %) "ba4")) (assoc % :bif "baf") %) 
  [[{:foo "ba1"} {:foo "ba2"}] [{:foo "ba3"} {:foo "ba4"}]])
;;=> [[{:foo "ba1"} {:foo "ba2"}] [{:foo "ba3"} {:foo "ba4", :bif "baf"}]]

And it works no matter how nested it gets:

(clojure.walk/postwalk #(if (and (map? %) (= (:foo %) "ba4")) (assoc % :bif "baf") %) 
  [[{:foo "ba1"} {:foo "ba2"}] [[{:foo "ba3"} {:test {:foo "ba4"}}]]])
;;=> [[{:foo "ba1"} {:foo "ba2"}] [[{:foo "ba3"} {:test {:foo "ba4", :bif "baf"}}]]]

Why postwalk instead of prewalk? Both work.

(clojure.walk/prewalk #(if (and (map? %) (= (:foo %) "ba4")) (assoc % :bif "baf") %) 
  [[{:foo "ba1"} {:foo "ba2"}] [{:foo "ba3"} {:foo "ba4"}]])
;;=> [[{:foo "ba1"} {:foo "ba2"}] [{:foo "ba3"} {:foo "ba4", :bif "baf"}]]

You don't have to lein install anything, and it's part of Clojure; you just have to require it.

Upvotes: 2

Rulle
Rulle

Reputation: 4901

Clojure is quite convenient for defining mini-languages for these sorts of things. A general and fairly reusable solution does not have to be much longer than a specialized one. Here is one suggestion if you are not interested in using a library.

Essentially, you want a function to update a datastructure. This function could be expressed using elementary functions of the same kind that serve as building blocks, making up a mini-language that I referred to before. In this example, the building blocks will be vector-scanner and map-decorator. We combine these functions into my-path that will perform the full update.

;; Path building blocks
(defn vector-scanner [subpath]
  #(mapv subpath %))

(defn map-decorator [k v decoration]
  #(if (and (map? %) (= v (k %)))
     (merge % decoration)
     %))

;; Function to do the update
(def my-path (-> (map-decorator :foo "ba4" {:bif "baf"})
                 vector-scanner
                 vector-scanner))

;; Running it
(my-path [[{:foo "ba1"}{:foo "ba2"}] [{:foo "ba3"}{:foo "ba4"}]])
;; => [[{:foo "ba1"} {:foo "ba2"}] [{:foo "ba3"} {:foo "ba4", :bif "baf"}]]

This approach is quite similar in spirit to how transducers can be combined in various configurations.

Upvotes: 2

Related Questions