使用 clojure,有没有更好的方法从序列中删除项目,这是地图中的值?

Using clojure, Is there a better way to to remove a item from a sequence, which is the value in a map?

有一张包含序列的地图。序列包含项目。 我想从包含它的任何序列中删除给定的项目。

我找到的解决方案做了它应该做的,但我想知道是否有更好的 或更优雅的方式来实现同样的目标。

我目前的解决方案:

(defn remove-item-from-map-value [my-map item]
    (apply merge (for [[k v] my-map] {k (remove #(= item %) v)})))

测试描述了预期的行为:

(require '[clojure.test :as t])

(def my-map {:keyOne   ["itemOne"]
             :keyTwo   ["itemTwo" "itemThree"]
             :keyThree ["itemFour" "itemFive" "itemSix"]})

(defn remove-item-from-map-value [my-map item]
  (apply merge (for [[k v] my-map] {k (remove #(= item %) v)})))

(t/is (= (remove-item-from-map-value my-map "unknown-item") my-map))
(t/is (= (remove-item-from-map-value my-map "itemFive") {:keyOne   ["itemOne"]
                                                         :keyTwo   ["itemTwo" "itemThree"]
                                                         :keyThree ["itemFour" "itemSix"]}))

(t/is (= (remove-item-from-map-value my-map "itemThree") {:keyOne   ["itemOne"]
                                                          :keyTwo   ["itemTwo"]
                                                          :keyThree ["itemFour" "itemFive" "itemSix"]}))

(t/is (= (remove-item-from-map-value my-map "itemOne") {:keyOne   []
                                                        :keyTwo   ["itemTwo" "itemThree"]
                                                        :keyThree ["itemFour" "itemFive" "itemSix"]}))

我是 clojure 的新手,对不同的解决方案很感兴趣。 所以欢迎任何意见。

我认为您的解决方案基本没问题,但我会尽量避免使用 apply merge 部分,因为您可以使用 into 轻松地从序列重新创建地图。此外,您还可以使用 map 而不是 for,我认为在这种情况下更符合习惯,因为您不使用 for 的任何列表理解功能。

(defn remove-item-from-map-value [m item]
    (->> m
         (map (fn [[k vs]]
                {k (remove #(= item %) vs)}))
         (into {})))

我会选择这样的东西:

user> (defn remove-item [my-map item]
        (into {}
              (map (fn [[k v]] [k (remove #{item} v)]))
              my-map))
#'user/remove-item

user> (remove-item my-map "itemFour")

;;=> {:keyOne ("itemOne"),
;;    :keyTwo ("itemTwo" "itemThree"),
;;    :keyThree ("itemFive" "itemSix")}

您还可以编写一个方便的函数 map-val 对映射值执行映射:

(defn map-val [f data]
  (reduce-kv
   (fn [acc k v] (assoc acc k (f v)))
   {} data))

或者像这样简短地说:

(defn map-val [f data]
  (reduce #(update % %2 f) data (keys data)))

user> (map-val inc {:a 1 :b 2})
;;=> {:a 2, :b 3}

(defn remove-item [my-map item]
  (map-val (partial remove #{item}) my-map))

user> (remove-item my-map "itemFour")
;;=> {:keyOne ("itemOne"),
;;    :keyTwo ("itemTwo" "itemThree"),
;;    :keyThree ("itemFive" "itemSix")}

我投入 specter 版本的好措施。它将矢量保存在地图内 而且真的很紧凑。

(setval [MAP-VALS ALL #{"itemFive"}] NONE my-map)

示例

user=> (use 'com.rpl.specter)
nil
user=> (def my-map {:keyOne   ["itemOne"]
  #_=>              :keyTwo   ["itemTwo" "itemThree"]
  #_=>              :keyThree ["itemFour" "itemFive" "itemSix"]})
  #_=> 
#'user/my-map
user=> (setval [MAP-VALS ALL #{"itemFive"}] NONE my-map)
{:keyOne ["itemOne"],
 :keyThree ["itemFour" "itemSix"],
 :keyTwo ["itemTwo" "itemThree"]}
user=> (setval [MAP-VALS ALL #{"unknown"}] NONE my-map)
{:keyOne ["itemOne"],
 :keyThree ["itemFour" "itemFive" "itemSix"],
 :keyTwo ["itemTwo" "itemThree"]}

另一个很像@leetwinski 的解决方案:

(defn remove-item [m i]
  (zipmap (keys m)
          (map (fn [v] (remove #(= % i) v))
               (vals m)))) 

这是一个以优雅的方式完成此操作的单行代码。我在这种情况下使用的完美功能是 clojure.walk/prewalk。这个 fn 所做的是遍历您传递给它的表单的所有子表单,并使用提供的 fn:

转换它们
(defn remove-item-from-map-value [data item] 
  (clojure.walk/prewalk #(if (map-entry? %) [(first %) (remove #{item} (second %))] %) data))

remove-item-from-map-value fn 会做的是检查当前表单是否是映射条目,如果是,它将从其值中删除指定的键(映射条目的第二个元素,它是一个向量分别包含一个键和一个值)。

这种方法最好的一点是它是完全可扩展的:您可以决定对不同类型的表单做不同的事情,您还可以处理嵌套表单等。

我花了一些时间来掌握这个 fn,但当我掌握它后,我发现它非常有用!