【问题标题】:How to merge maps and get a map of lists?如何合并地图并获取列表地图?
【发布时间】:2016-06-11 06:51:56
【问题描述】:

假设我们是一张地图列表。地图都有相同的关键字,但我们事先并不知道关键字。

[{:a 1 :b 2} {:a 3 :b 4}]

以及将这个列表合并到这样一个地图中的惯用方式是什么:

{:a [1 3]
 :b [2 4]}

看起来并不难,但是当我开始实现该功能时,它变得超级丑陋和重复。我有一种感觉,有更清洁的方法可以实现这一目标。

谢谢

【问题讨论】:

    标签: clojure functional-programming


    【解决方案1】:

    您实际上可以通过使用标准库中的几个函数来获得一个非常优雅的解决方案:

    (defn consolidate [& ms]
      (apply merge-with conj (zipmap (mapcat keys ms) (repeat [])) ms))
    

    例子:

    (consolidate {:a 1 :b 2} {:a 3 :b 4})
    ;=> {:a [1 3], :b [2 4]}
    

    这个解决方案的一个很酷的地方是,即使地图具有不同的键集,它也能正常工作。

    【讨论】:

    • 这是一个非常简短的解决方案,但直到我尝试了 REPL 中的每一个小部分,我才理解它。 mapcat 在那里非常有用。
    • 看起来很棒,即使参数类型不同:[ms] vs [& ms]。有没有办法在调用 consolidate 时将 ms 列表作为参数传播?
    • Oguz - 查看我对 Sam 解决方案的重写。
    • @OguzBilgic 是的;您可以将参数向量更改为 [ms] 或使用 apply: (apply consolidate [{:a 1 :b 2} {:a 3 :b 4}])
    【解决方案2】:

    我宁愿使用双重减少将它们与update“合并”:

    (defn merge-maps-with-vec [maps]
      (reduce (partial reduce-kv #(update %1 %2 (fnil conj []) %3))
              {} maps))
    
    user> (merge-maps-with-vec [{:a 1 :b 2} {:a 3 :b 4 :c 10}])
    {:a [1 3], :b [2 4], :c [10]}
    

    它不像@Sam Estep 的答案那样富有表现力,但另一方面它不会生成任何中间序列(例如每个键到空向量映射,它还需要通过每个地图)。当然,过早的优化通常是不好的,但我想它不会在这里受到伤害。虽然基于 reduce 的解决方案看起来有点晦涩难懂,但如果将其放入带有适当文档的库中,最终用户(或一年后的您自己)不会觉得它晦涩难懂

    【讨论】:

      【解决方案3】:

      虽然有许多解决方案是可能的,但这里有一个使用一些便利功能in the Tupelo library

      (ns clj.core
        (:use tupelo.core)
        (:require [tupelo.schema :as ts]
                  [schema.core :as s] ))
      
      (s/defn gather-keys
        [list-of-maps :- [ts/KeyMap]]
        (newline)
        (let [keys-vec (keys (first list-of-maps))]
          (s/validate [s/Keyword] keys-vec) ; verify it is a vector of keywords
          (apply glue
            (for [curr-key keys-vec]
              {curr-key (forv [curr-map list-of-maps]
                          (get curr-map curr-key))} ))))
      (deftest t-maps
        (spyx
          (gather-keys [{:a 1 :b 2}
                        {:a 3 :b 4} ] )))
      
      (gather-keys [{:a 1, :b 2} {:a 3, :b 4}]) ;=> {:a [1 3], :b [2 4]}
      

      请注意,此解决方案假设每个输入映射都有一组相同的键。通常我也想通过代码中的健全性检查来强制执行该假设。

      看着 Sam 的答案,我会用一些临时变量重写它以帮助记录子步骤:

      (defn consolidate-keys [list-of-maps]
        (let [keys-set     (set (mapcat keys list-of-maps))
              base-result  (zipmap keys-set  (repeat [] )) ]
          (apply merge-with conj base-result list-of-maps)))
      
      (consolidate-keys  [ {:a 1 :b 2}
                           {:a 3 :z 9} ] )
      ;=> {:z [9], :b [2], :a [1 3]}
      

      【讨论】:

      • 我已经接受了他的回答,不过谢谢你的重写,更容易理解。
      • 为什么在每个答案中都包含tupelo 库?它只会让任何人更难实际使用您的代码,而且通常不会导致任何更好的解决方案。
      • 我并不打算将我的解决方案作为“技巧答案”...如果造成任何混淆,我深表歉意。
      • Sam:我很少看到mapcatzipmap 被使用过,所以我总是不得不停下来弄清楚到底发生了什么,尤其是。在这种情况下,因为mapcat 生成了许多重复的密钥,zipmap 自动删除了重复数据。在我在 repl 中制作原型之前,这真是令人头疼。
      • amalloy:原生 clojure 中的许多函数对于新用户来说很容易出错,并且经常容易出错。许多人还具有“模糊”和非直觉的行为(参见github.com/cloojure/tupelo#gluing-together-like-collections 示例)。 Tupelo 库旨在使 Clojure 更简单、更可预测且更安全,尤其是对于新用户而言。每次我向新用户展示它时,他们都会惊讶于核心 Clojure 中不存在这些功能。
      猜你喜欢
      • 2019-05-02
      • 2020-10-26
      • 2017-12-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多