Improving the replacement of complex data structures

I am trying to change a specific field in the data structure described below (a completed example can be found here :

[{:fields "There are a few other fields here"
:incidents [{:fields "There are a few other fields here"
             :updates [{:fields "There are a few other fields here"
                        :content "THIS is the field I want to replace"
                        :translations [{:based_on "Based on the VALUE of this" 
                                        :content "Replace with this value"}]}]}]}]

      

I have already implemented this in several functions as shown below:

(defn- translation-content
  [arr]
  (:content (nth arr (.indexOf (map :locale arr) (env/get-locale)))))

(defn- translate
  [k coll fn & [k2]]
  (let [k2 (if (nil? k2) k k2)
        c ((keyword k2) coll)]
    (assoc-in coll [(keyword k)] (fn c))))

(defn- format-update-translation
  [update]
  (dissoc update :translations))

(defn translate-update
  [update]
  (format-update-translation (translate :content update translation-content :translations)))

(defn translate-updates
  [updates]
  (vec (map translate-update updates)))

(defn translate-incident
  [incident]
  (translate :updates incident translate-updates))

(defn translate-incidents
  [incidents]
  (vec (map translate-incident incidents)))

(defn translate-service
  [service]
  (assoc-in service [:incidents] (translate-incidents (:incidents service))))

(defn translate-services
  [services]
  (vec (map translate-service services)))

      

Each array can have any number of entries (although the number is probably less than 10).

The basic premise is to replace :content

each :update

with the appropriate :translation

one based on the supplied value.

My Clojure knowledge is limited, so I'm wondering if there is a better way to achieve this?

EDIT Solution so far:

(defn- translation-content
  [arr]
  (:content (nth arr (.indexOf (map :locale arr) (env/get-locale)))))

(defn- translate
  [k coll fn & [k2]]
  (let [k2 (if (nil? k2) k k2)
        c ((keyword k2) coll)]
    (assoc-in coll [(keyword k)] (fn c))))

(defn- format-update-translation
  [update]
  (dissoc update :translations))

(defn translate-update
  [update]
  (format-update-translation (translate :content update translation-content :translations)))

(defn translate-updates
  [updates]
  (mapv translate-update updates))

(defn translate-incident
  [incident]
  (translate :updates incident translate-updates))

(defn translate-incidents
  [incidents]
  (mapv translate-incident incidents))

(defn translate-service
  [service]
  (assoc-in service [:incidents] (translate-incidents (:incidents service))))

(defn translate-services
  [services]
  (mapv translate-service services))

      

+3


source to share


3 answers


I would start more or less like you, from the bottom up, defining some functions that look like they would be useful: how to select a translation from a list of translations and how to apply that selection to an update.But I would not do such tiny functions. like yours: logic spreads to many places and it is not easy to get a general idea of ​​what is going on. Here are the two functions I started out with:

(letfn [(choose-translation [translations]
          (let [applicable (filter #(= (:locale %) (get-locale))
                                   translations)]
            (when (= 1 (count applicable))
              (:content (first applicable)))))
        (translate-update [update]
          (-> update
              (assoc :content (or (choose-translation (:translations update))
                                  (:content update)))
              (dissoc :translations)))]
  ...)

      

Of course you can have defn

them instead if you want, and I suspect a lot of people will, but they will only be used in one place and they are closely related to the context in which they're used, which is why I like it letfn

. These two functions are really all interesting logic; the rest is just some boring tree traversal code to apply this logic in the right places.

Now building the body is letfn

simple and easy to read if you create the code in the same form as the data it manipulates. We want to go through a series of nested lists, updating objects along the way, and so we just write a series of nested for

concepts calling update

to descend into the correct keyspace:



    (for [user users]
      (update user :incidents
              (fn [incidents]
                (for [incident incidents]
                  (update incident :updates
                          (fn [updates]
                            (for [update updates]
                              (translate-update update))))))))

      

I think the usage for

here is much better than the usage map

, although of course they are equivalent as always. The important difference is that when you read the code, you see the new context first ("okay, now we're doing something for every user"), and then what happens inside that context; with the help map

you see them in a different order and it is difficult to keep what is going on there.

By combining them and putting them in defn

, we get a function that you can call with your example input and that produces the desired output (assuming a suitable definition get-locale

):

(defn translate [users]
  (letfn [(choose-translation [translations]
            (let [applicable (filter #(= (:locale %) (get-locale))
                                     translations)]
              (when (= 1 (count applicable))
                (:content (first applicable)))))
          (translate-update [update]
            (-> update
                (assoc :content (or (choose-translation (:translations update))
                                    (:content update)))
                (dissoc :translations)))]
    (for [user users]
      (update user :incidents
              (fn [incidents]
                (for [incident incidents]
                  (update incident :updates
                          (fn [updates]
                            (for [update updates]
                              (translate-update update))))))))))

      

+3


source


we can try to find some templates in this task (based on the content of the snippet from the github gist you posted):

you just need

1) update every item (A) in the data vector

2) updating each item (B) in vector A: incidents

3) updating every item (C) in vector B: updates

4) translating C

The function translate

might look like this:

(defn translate [{translations :translations :as item} locale]
  (assoc item :content
         (or (some #(when (= (:locale %) locale) (:content %)) translations)
             :no-translation-found)))

      

this usage (some fields omitted for brevity):

user> (translate {:id 1
                  :content "abc"
                  :severity "101"
                  :translations [{:locale "fr_FR"
                                  :content "abc"}
                                 {:locale "ru_RU"
                                  :content ""}]}
                 "ru_RU")
;;=> {:id 1,
;;    :content "", 
;;    :severity "101", 
;;    :translations [{:locale "fr_FR", :content "abc"} {:locale "ru_RU", :content ""}]}

      



then we see that 1 and 2 are completely similar, so we can generalize that:

(defn update-vec-of-maps [data k f]
  (mapv (fn [item] (update item k f)) data))

      

using it as a building block, you can compose the whole data transformation:

(defn transform [data locale]
  (update-vec-of-maps
    data :incidents
    (fn [incidents]
      (update-vec-of-maps
        incidents :updates
        (fn [updates] (mapv #(translate % locale) updates))))))

(transform data "it_IT")

      

returns what you need.

then you can generalize it further by making a utility function for arbitrary depth transformations:

(defn deep-update-vec-of-maps [data ks terminal-fn]
  (if (seq ks)
    ((reduce (fn [f k] #(update-vec-of-maps % k f))
             terminal-fn (reverse ks))
     data)
    data))

      

and use it like this:

(deep-update-vec-of-maps data [:incidents :updates]
                         (fn [updates]
                           (mapv #(translate % "it_IT") updates)))

      

+3


source


I recommend you look at https://github.com/nathanmarz/specter

This makes it easier to read and update clojure data structures. Same performance as handwritten code, but much shorter.

+1


source







All Articles