"join" 两个地图列表是否有 clojure 函数?

Is there a clojure function to "join" two list of maps?

我正在寻找一个类似于 sql 中的连接函数,例如:

这是两个地图列表:

(def a [{:user_id 1 :name "user 1"} 
        {:user_id 2 :name "user 2"}])

(def b [{:user_id 2 :email "e 2"} 
        {:user_id 1 :email "e 1"}])

我想在 user_id 加入 a 和 b 以获得:

[{:user_id 1 :name "user 1" :email "e 1"} 
 {:user_id 2 :name "user 2" :email "e 2"}]

clojure 或其他库中是否有一些函数可以实现此目的?

我认为没有任何简单的函数可以做到这一点,但我可能是错的。

如果你知道每个user_id存在于每个序列中,那么你可以只对user_id进行排序,然后将合并应用到相应的映射:

(defn sort-by-user-id 
  [m]
  (sort #(< (:user_id %1) (:user_id %2)) m))

(map merge (sort-by-user-id a) (sort-by-user-id b))
; => ({:email "e 1", :name "user 1", :user_id 1} {:email "e 2", :name "user 2", :user_id 2})

如果您不能假设每个序列中都存在所有相同的 user_id,我认为您需要做一些稍微复杂的事情才能匹配 user_id。我假设如果名称映射没有相应的电子邮件映射,您希望保留名称映射不变(反之亦然,缺少名称映射)。如果没有,那么一种选择是去除这些地图并使用上面给出的方法。

这是合并相应名称和电子邮件映射的一种方法。我们可以使用 user_id 作为地图地图中的键来匹配相应的地图。首先创建包含所有以 user_ids 作为键的地图的地图,例如,像这样:

(def az (zipmap (map :user_id a) a)) ; => {2 {:name "user 2", :user_id 2}, 1 {:name "user 1", :user_id 1}}
(def bz (zipmap (map :user_id b) b)) ; => {1 {:email "e 1", :user_id 1}, 2 {:email "e 2", :user_id 2}}

然后像这样合并各个映射,在过程结束时删除键:

(vals (merge-with merge az bz))
; => ({:email "e 2", :name "user 2", :user_id 2} {:email "e 1", :name "user 1", :user_id 1})

综合起来:

(defn map-of-maps
  [cm]
  (zipmap (map :user_id cm) cm))

(defn merge-maps
  [& cms]
  (vals 
    (apply merge-with merge 
           (map map-of-maps cms))))

让我们确保它适用于缺少的 user_ids:

(def a+ (conj a {:name "user 3", :user_id 3}))
(def b+ (conj b {:email "e 4", :user_id 4}))

(merge-maps a+ b+)
; => ({:email "e 4", :user_id 4} {:name "user 3", :user_id 3} {:email "e 2", :name "user 2", :user_id 2} {:email "e 1", :name "user 1", :user_id 1})

如果有更简单或更优雅的方法,我不会感到惊讶。这只是我想到的一种策略。

另一个选择,我觉得简单一点:

user=> (map #(apply merge %) (vals (group-by :user_id (concat a b))))
({:email "e 1", :name "user 1", :user_id 1} {:email "e 2", :name "user 2", :user_id 2})

group-by 创建从 :user_id 到包含给定值的所有映射的映射,vals 仅获取值(每个值都是一个向量),最后对于每个向量值,它们被合并。

clojure.set/join 会做的。

(require '[clojure.set :as set])

(set/join a b) ; => #{{:email "e 1", :name "user 1", :user_id 1} {:email "e 2", :name "user 2", :user_id 2}}

在不提供第三个参数的情况下,函数将加入所有公共键:

(def a [{:id1 1 :id2 2 :name "n 1"} {:id1 2 :id2 3 :name "n 2"}])
(def b [{:id1 1 :id2 2 :url "u 1"} {:id1 2 :id2 4 :url "u 2"}])
(def c [{:id1 1 :id2 2 :url "u 1"} {:id1 2 :url "u 2"}]) ; :id2 is missing in 2nd record

(set/join a b) ; #{{:name "n 1", :url "u 1", :id1 1, :id2 2}}
(set/join a c) ; #{{:name "n 2", :url "u 2", :id1 2, :id2 3} {:name "n 1", :url "u 1", :id1 1, :id2 2}}

仅在 id1 上加入 a 和 b:

(set/join a b {:id1 :id1}) ; #{{:name "n 2", :url "u 2", :id1 2, :id2 4} {:name "n 1", :url "u 1", :id1 1, :id2 2}}

我们甚至可以通过来自不同集合的不同键加入:

(set/join a b {:id1 :id2}) ; #{{:name "n 2", :url "u 1", :id1 1, :id2 2}}