改进复杂的数据结构替换

时间:2017-06-16 19:23:36

标签: clojure

我正在尝试修改数据结构中的特定字段,如下所述(可以找到填充示例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"}]}]}]}]

我已经在许多函数中实现了它,如下所示:

(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)))

每个数组可以包含任意数量的条目(尽管数量可能小于10)。

基本前提是根据提供的值替换每个:content中的:update和相关的:translation

我的Clojure知识是有限的,所以我很好奇是否有更优化的方法来实现这一目标?

修改 解决方案到目前为止:

(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 个答案:

答案 0 :(得分:3)

我会或多或少地开始,自下而上,通过定义一些看起来有用的功能:如何从翻译列表中选择翻译,以及如何将该选择应用于更新。但是我不会让你的功能变得如此微小:逻辑分散在很多地方,并且要弄清楚发生了什么并不容易。以下是我开始使用的两个函数:

(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)))]
  ...)

当然,如果您愿意,可以defn代替他们,我怀疑很多人会这样做,但他们只会在一个地方使用,而且他们密切关注它们被使用了,所以我喜欢letfn。这两个函数真的都是有趣的逻辑;其余的只是一些无聊的树遍历代码,可以在正确的位置应用这个逻辑。

现在构建letfn的主体很简单,如果你使你的代码与它操作的数据形状相同,则易于阅读。我们想要遍历一系列嵌套列表,在路上更新对象,因此我们只编写一系列嵌套的for推理,调用update进入正确的键空间:

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

我认为在这里使用for比使用map好几英里,尽管它们当然一如既往。重要的区别在于,当您阅读代码时,您首先会看到新的上下文(“好吧,现在我们正在为每个用户做一些事情”),然后在该上下文中发生的事情;使用map,您可以按照其他顺序看到它们,并且难以随时了解发生的情况。

组合这些并将它们放入defn,我们得到一个函数,您可以使用示例输入调用它并生成所需的输出(假设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))))))))))

答案 1 :(得分:3)

我们可以尝试在此任务中找到一些模式(基于github gist的片段内容,您已发布):

简单来说,你需要

1)更新数据向量中的每个项目(A)

2)更新A的矢量中的每个项目(B):事件

3)更新B的向量中的每个项目(C):更新

4)翻译C

translate函数可能如下所示:

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

它的用法(为简洁起见省略了一些字段):

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 "абв"}]}

然后我们可以看到1和2完全相似,所以我们可以概括:

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

使用它作为构建块,您可以构成整个数据转换:

(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")

返回您需要的内容。

然后你可以进一步概括它,使任务深度转换的效用函数:

(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))

并像这样使用它:

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

答案 2 :(得分:1)

我建议您查看https://github.com/nathanmarz/specter

它使得查看和更新​​clojure数据结构变得非常容易。与手写代码的性能相同,但更短。