Clojure中如何基于自定义比较实现set/union(仅用:id与:name)
好问题!在Clojure里处理这种只关心部分字段的集合合并,默认的clojure.set/union确实帮不上忙——它是拿整个元素的相等性来判断的,所以哪怕只是zip不一样,也会被当成两个不同元素。下面是几种业内惯用的解决办法,你可以根据场景选:
方法1:基于唯一键映射合并(最常用、高效)
这种思路是把每个元素的:id和:name组合成唯一键,用Map来自动去重(因为Map的键是唯一的),最后提取值转成集合就行。优点是简洁、性能好,不需要额外定义类型:
(def set-a #{{:id 1 :name "ABC" :zip 78759} {:id 2 :name "DEF" :zip 78759}}) (def set-b #{{:id 1 :name "ABC" :zip 78753} {:id 3 :name "XYZ" :zip 78704}}) (defn union-by-id-name [s1 s2] (->> (concat s1 s2) ;; 用[id, name]作为唯一键,自动去重 (reduce (fn [acc elem] (assoc acc [(elem :id) (elem :name)] elem)) {}) vals ;; 提取去重后的元素 set)) ;; 转成集合 (union-by-id-name set-a set-b) ;; 输出:#{{:id 3, :name "XYZ", :zip 78704} {:id 1, :name "ABC", :zip 78759} {:id 2, :name "DEF", :zip 78759}}
如果想保留第一个出现的重复元素(而不是最后一个覆盖),可以把assoc改成条件判断:
(reduce (fn [acc elem] (let [key [(elem :id) (elem :name)]] (if (contains? acc key) acc (assoc acc key elem)))) {} (concat s1 s2))
方法2:自定义相等性的记录类型(适合多次集合操作)
如果你需要反复基于:id和:name做集合操作(比如交集、差集),可以定义一个自定义记录类型,重写它的equals和hashCode方法,让集合默认的相等性判断只看这两个字段:
(defrecord Entity [id name zip] Object ;; 只比较id和name (equals [this other] (and (instance? Entity other) (= (:id this) (:id other)) (= (:name this) (:name other)))) ;; 只基于id和name生成哈希值 (hashCode [this] (hash [(:id this) (:name this)]))) ;; 把原集合的元素转换成自定义记录 (def set-a-entities (set (map ->Entity set-a))) (def set-b-entities (set (map ->Entity set-b))) ;; 现在可以直接用默认的union了 (clojure.set/union set-a-entities set-b-entities) ;; 如果需要转回原来的Map格式: (set (map #(into {} %) *1)) ;; 输出和方法1一致
这种方法的好处是一旦定义好记录类型,所有标准集合操作(union/intersection/difference)都能直接用,不用再写自定义函数。缺点是需要额外的类型转换步骤。
方法3:过滤法(适合小集合)
如果你的集合规模很小,也可以用过滤的方式:先提取第一个集合的id+name键,然后过滤掉第二个集合中重复的元素,再合并:
(defn union-by-id-name [s1 s2] (let [existing-keys (set (map #(select-keys % [:id :name]) s1)) ;; 找出s2中不在s1里的新元素 s2-new (filter #(not (contains? existing-keys (select-keys % [:id :name]))) s2)] (set (concat s1 s2-new))))
这种方法逻辑直观,但性能不如方法1——因为每次过滤都要做线性查找,集合大的时候效率会下降。
内容的提问来源于stack exchange,提问作者sakh1979
相关产品推荐
相关产品推荐

