Clojure惯用法更新地图的多个值

时间:2015-09-21 02:51:28

标签: dictionary clojure reduce

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

(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)会有效,但在def上的函数中使用map似乎并不合适。 使m成为原子版并使用swap!似乎更好。但不是很多它似乎也很慢。

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

这样做的最佳/正确方法是什么?

更新

这里有一些很棒的答案。我在这里发布了一个跟进问题Clojure: iterate over map of sets,与此问题密切相关,但没有那么多。

3 个答案:

答案 0 :(得分:6)

在一个函数中使用def是不对的。在swap内使用带有副作用的函数(例如map)也是一种不好的形式。此外,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)

答案 1 :(得分:2)

当然不建议在函数内部使用def。查看问题的OO方法是查看数据结构并修改值。查看问题的功能方法是构建一个新的数据结构,该结构表示值已更改的旧数据结构。所以我们可以做这样的事情

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

我们在这里做的是映射m,就像你在第一个例子中所做的那样。但是,我们不是修改m而是返回[]元组的键和值。这个元组列表然后我们可以放回到地图中。

答案 2 :(得分:1)

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

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