【问题标题】:Update nested structure of maps and vectors更新地图和向量的嵌套结构
【发布时间】:2017-02-19 21:33:39
【问题描述】:

我有一张这样的地图矢量:

{:tags    ["type:something" "gw:somethingelse"],
 :sources [{:tags    ["s:my:tags"],
            :metrics [{:tags    ["a tag"]}
                      {:tags    ["a noether tag" "aegn"]}
                      {:tags    ["eare" "rh"]}]}]}

请注意,可以有多个来源和多个指标。

现在我想通过查看标签的值将:metrics 更新为id

示例:如果 ["a tag"] 与例如 id 1 匹配,["a noether tag" "aegn"] 与 id 2 匹配,我希望更新后的结构如下所示:

{:tags    ["type:something" "gw:somethingelse"],
 :sources [{:tags    ["s:my:tags"],
            :metrics [{:tags    ["a tag"]
                       :id      1}
                      {:tags    ["a noether tag" "aegn"]
                       :id      2}
                      {:tags    ["eare" "rh"]}]}]}

我做了一个函数transform,可以将标签转换为id。例如,(transform "a tag") 返回 1。

现在,当我尝试添加带有理解的 id 时,我想念旧结构(只返回内部结构)并且使用 assoc-in 我必须预先知道索引。

我怎样才能优雅地执行这种转换?

【问题讨论】:

  • 在编辑您的问题以平衡括号时,我担心我可能进行了错误的修复。原始数据和所需数据都有一个只有一个元素的:sources 向量。它是一个向量,还是我应该去掉多余的左方括号?
  • :sources 的值是映射向量。现在的方式在我看来不错。

标签: clojure


【解决方案1】:

我会从下往上开始,为:tags 条目创建转换函数,然后为:metrics:sources

假设我们的转换函数仅通过计算标签来生成 id(只是为了说明,以后可以轻松更改):

(defn transform [tags] (count tags))

user> (transform ["asd" "dsf"])
;;=> 2

然后对度量条目应用转换:

(defn transform-metric [{:keys [tags] :as m}]
  (assoc m :id (transform tags)))

user> (transform-metric {:tags    ["a noether tag" "aegn"]})
;;=> {:tags ["a noether tag" "aegn"], :id 2}

现在使用transform-metric 更新源条目:

(defn transform-source [s]
  (update s :metrics #(mapv transform-metric %)))

user> (transform-source {:tags    ["s:my:tags"],
                         :metrics [{:tags    ["a tag"]}
                                   {:tags    ["a noether tag" "aegn"]}
                                   {:tags    ["eare" "rh"]}]})

;;=> {:tags ["s:my:tags"], 
;;    :metrics [{:tags ["a tag"], :id 1} 
;;              {:tags ["a noether tag" "aegn"], :id 2} 
;;              {:tags ["eare" "rh"], :id 2}]}

最后一步是转换整个数据:

(defn transform-data [d]
  (update d :sources #(mapv transform-source %)))

user> (transform-data data)
;;=> {:tags ["type:something" "gw:somethingelse"], 
;;    :sources [{:tags ["s:my:tags"], 
;;               :metrics [{:tags ["a tag"], :id 1} 
;;                         {:tags ["a noether tag" "aegn"], :id 2}
;;                         {:tags ["eare" "rh"], :id 2}]}]}

所以,我们到此结束。

现在注意到transform-datatransform-source 几乎相同,所以我们可以创建一个实用函数来生成这样的更新函数:

(defn make-vec-updater [field transformer]
  (fn [data] (update data field (partial mapv transformer))))

使用这个函数,我们可以像这样定义深度转换:

(def transformer
  (make-vec-updater
   :sources
   (make-vec-updater
    :metrics
    (fn [{:keys [tags] :as m}]
      (assoc m :id (transform tags))))))

user> (transformer data)
;;=> {:tags ["type:something" "gw:somethingelse"], 
;;    :sources [{:tags ["s:my:tags"], 
;;               :metrics [{:tags ["a tag"], :id 1} 
;;                         {:tags ["a noether tag" "aegn"], :id 2}
;;                         {:tags ["eare" "rh"], :id 2}]}]}

并且基于这种转换器构造方法,我们可以创建一个很好的函数来更新vectors-of-maps-of-vectors-of-maps-of-vectors...结构中的值,具有任意嵌套级别:

(defn update-in-v [data ks f]
  ((reduce #(make-vec-updater %2 %1) f (reverse ks)) data))

user> (update-in-v data [:sources :metrics]
                   (fn [{:keys [tags] :as m}] 
                     (assoc m :id (transform tags))))

;;=> {:tags ["type:something" "gw:somethingelse"], 
;;    :sources [{:tags ["s:my:tags"], 
;;               :metrics [{:tags ["a tag"], :id 1} 
;;                         {:tags ["a noether tag" "aegn"], :id 2} 
;;                         {:tags ["eare" "rh"], :id 2}]}]}

更新

除了这种手动方法之外,还有一个名为 specter 的出色库,它做的事情完全相同(还有更多),但显然更通用和可用:

(require '[com.rpl.specter :as sp])

(sp/transform [:sources sp/ALL :metrics sp/ALL]
              (fn [{:keys [tags] :as m}] 
                (assoc m :id (transform tags)))
              data)

;;=> {:tags ["type:something" "gw:somethingelse"], 
;;    :sources [{:tags ["s:my:tags"], 
;;               :metrics [{:tags ["a tag"], :id 1} 
;;                         {:tags ["a noether tag" "aegn"], :id 2} 
;;                         {:tags ["eare" "rh"], :id 2}]}]}

【讨论】:

  • 不错!这是一个很好的答案。
【解决方案2】:
(use 'clojure.walk)

(def transform {["a tag"]                1
                ["a noether tag" "aegn"] 2})

(postwalk #(if-let [id (transform (:tags %))]
            (assoc % :id id)
            %)
          data)

输出:

{:tags ["type:something" "gw:somethingelse"],
:sources
[{:tags ["s:my:tags"],
  :metrics
  [{:tags ["a tag"], :id 1}
    {:tags ["a noether tag" "aegn"], :id 2}
    {:tags ["eare" "rh"]}]}]}

【讨论】:

  • 不错的方法,但如果 transform 函数为它们返回了一些 id 值,它也可以转换不需要的 :tags 值(例如顶级 :tags)...
  • 可以使用一个简单的函数(或使用 clojure.spec)在进行转换之前检查一致性。例如。 (defn is-metric? [{:keys [tags sources metrics]}] (and tags (not (or sources metrics))))
  • 我喜欢这种方法!谢谢。
猜你喜欢
  • 1970-01-01
  • 2014-09-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多