Clojure 惯用的方式来更新地图的多个值

Clojure idiomatic way to update multiple values of map

这可能很简单,但我就是无法克服它。 我有一个嵌套映射的数据结构,如下所示:

(def m {:1 {:1 2 :2 5 :3 10} :2 {:1 2 :2 50 :3 25} :3 {:1 42 :2 23 :3 4}})

我需要设置每个 m[i][i]=0。这在非函数式语言中很简单,但我无法让它在 Clojure 上运行。考虑到我确实有一个包含所有可能值的向量,惯用的方法是怎样的? (我们称它为 v

执行 (map #(def m (assoc-in m [% %] 0)) v) 会起作用,但在 map 上的函数中使用 def 似乎不正确。 将 m 变成原子版本并使用 swap! 似乎更好。但不多它也似乎真的很慢。

(def am (atom m))
(map #(swap! am assoc-in[% %] 0) v)

best/right 方法是什么?

更新

这里有一些很棒的答案。我在这里 发布了一个后续问题,它与这个问题密切相关,但关系不大。

当然不推荐在函数内部使用def。查看问题的 OO 方法是查看数据结构内部并修改值。看待这个问题的功能性方法是建立一个新的数据结构来代表旧的数据结构,其中的值发生了变化。所以我们可以做这样的事情

(into {} (map (fn [[k v]] [k (assoc v k 0)]) m))

我们在这里所做的是在 m 上进行映射,其方式与您在第一个示例中所做的方式大致相同。但是然后我们 return 不是修改 m 一个 [] 键和值的元组。然后我们可以将这个元组列表放回映射中。

你说得对,在函数内部使用 def 是一种错误的形式。在 map 中使用具有副作用的函数(例如 swap)也是一种不好的形式。此外,map 是惰性的,因此您的 map/swap 尝试实际上不会做任何事情,除非它被强制使用,例如 dorun.

现在我们已经介绍了要做的事情,让我们来看看如何去做;-)。

您可以采用多种方法。对于来自命令式范式的人来说,最简单的开始可能是 loop:

(defn update-m [m v]
  (loop [v' v
         m' m]
    (if (empty? v')
      ;; then (we're done => return result)
      m'
      ;; else (more to go => process next element)
      (let [i (first v')]
        (recur (rest v')                  ;; iterate over v
               (assoc-in m' [i i] 0)))))) ;; accumulate result in m'

但是,loop 是函数范式中相对较低级别的结构。这里我们可以注意到一个模式:我们正在遍历 v 的元素并累积 m' 中的变化。此模式由 reduce 函数捕获:

(defn update-m [m v]
  (reduce (fn [m' i]
            (assoc-in m' [i i] 0)) ;; accumulate changes
          m   ;; initial-value
          v)) ;; collection to loop over

此表单要短一些,因为它不需要 loop 表单所需的样板代码。 reduce 形式一开始可能不太好读,但是一旦你习惯了函数式代码,它就会变得更加自然。

现在我们有了 update-m,我们可以用它来在整个程序中转换地图。例如,我们可以用它来 swap! 一个原子。按照上面的示例:

(swap! am update-m v)

其他答案都很好,但为了完整起见,这里有一个使用 for 理解的略短版本。我发现它更具可读性,但这只是个人喜好问题:

(into {} (for [[k v] m] {k (assoc v k 0)}))