【问题标题】:In Clojure, How do I update a nested map correctly?在 Clojure 中,如何正确更新嵌套地图?
【发布时间】:2015-12-20 01:20:45
【问题描述】:

经过多年的 Java(和 PHP/JavaScript)经验,我刚刚开始学习 Clojure。多么大的挑战:-)

如何以惯用方式更新值映射?当我在地图上使用map 函数时,它不会返回地图,而是返回一个序列。

我正在开发一个小型应用程序,其中有一个任务列表。我想做的是更改某些单独任务中的一些值,然后更新原始任务列表。以下是我正在测试的任务:

(defrecord Task [key name duration])

(def tasks
  (atom
    {
     "t1" (->Task "t1" "Task 1" 10)
     "t2" (->Task "t2" "Task 2" 20)
     "t3" (->Task "t3" "Task 3" 30)
     }
    ))

我使用字符串键将任务放入哈希映射中,因此它可以快速、直接地访问映射中的任何任务。每个任务也都拥有关键,所以当我将单个任务传递给其他函数时,我知道关键是什么。

为了更新持续时间,我使用mapupdate-in 迭代并有选择地更新每个任务的持续时间,并返回修改后的任务。

函数如下:

(defn update-task-durations
  "Update the duration of each task and return the updated tasks"
  [tasks]
  ; 1) Why do I have to convert the result of the map function,
  ;    from a sequence then back to a map?
  (into {}
    (map
      (fn [task]
        (println task) ; debug
        (update-in
          task
          ; 2) Why do I have to use vector index '1' here
          ;    to get the value of the map entry?
          [1 :duration]
          (fn [duration]
            (if (< duration 20)
              (+ duration 1)
              (+ duration 2)
              )
            )
          )
        ) tasks))
  )

我用这个打印之前/之后的值:

(println "ORIGINAL tasks:")
(println @tasks)

(swap! tasks update-task-durations)

(println "\nUPDATED tasks:")
(println @tasks)

1) 我遇到的主要问题是 map 函数返回一个序列,而不是一个映射,所以我不得不再次使用 into {} 将序列转换回映射,这在我看来是不必要的和低效的。

有没有更好的方法来做到这一点?我应该使用map以外的函数吗?

我能否更好地安排我的数据结构,同时仍然有效地直接访问单个任务?

可以使用into {} 将(可能非常大的)序列转换为地图吗?

2) 此外,在我传递给map 函数的函数参数中,每个任务都由map 提供给我,作为[key value] 形式的向量,当我期望一个映射条目时,所以要从映射条目中获取值,我必须将以下键传递给我的 update-in [1 :duration] 这看起来有点难看,有没有更好/更清晰的方法来访问映射条目而不是使用索引 1向量?

【问题讨论】:

    标签: dictionary clojure update-in


    【解决方案1】:

    解决这个映射映射问题的流行方法是使用zipmap

    (defn map-vals
      "Returns the map with f applied to each item."
      [f m]
      (zipmap (keys m)
              (map f (vals m))))
    
    (defn update-task-durations
      [tasks]
      (let [update-duration (fn [duration]
                              (if (< duration 20)
                                (+ 1 duration)
                                (+ 2 duration)))]
        (->> tasks
             (map-vals #(update % :duration update-duration)))))
    
    (swap! tasks update-task-durations)
    

    对于 Clojure (update-in % [:duration] ...。

    或者,您也可以使用解构来简化您当前的解决方案,而无需定义实用函数:

    (->> tasks
         (map (fn [[k task]]
                [k (update task :duration update-duration)]))
         (into {})
    

    为什么?

    map 只处理序列。如果您喜欢类型签名,这意味着map 始终具有相同的类型(map :: (a -&gt; b) -&gt; [a] -&gt; [b]),但这也意味着您从map 中得到的只是一个seq-of-something。

    这里不要太担心效率。 into 速度很快,而且非常地道。

    【讨论】:

    • 感谢您的回答。我喜欢实用函数的想法,将更新函数作为参数,使地图/更新更通用。 zipmap 看起来非常强大,但我有点担心它如何提取所有键、所有值,然后构建一个新地图——这是实现这一点的有效方法吗? (我所有的 Java 经验都告诉我要避免这种提取和重建!)
    • @SteveMoseley 最高效的方式可能是将reduce-kvtransients 结合起来——您可以在REPL 中使用(time ...) 进行基本计时进行实验,并使用(source ...) 查看源代码.但是keysvals 都只返回由原始映射支持的迭代器(seqs),而zipmap 本质上是(assoc map k v) while hasNext(key and val),完全滥用Java-ish 语法。无论如何,它最终还是相当有效的,因为 Clojure 已经为此进行了优化——所有这些不可变对象之间在后台有很多结构共享。
    • 瞬态听起来像是一个很好的工具,感谢您的链接。由于我对 Clojure 还很陌生,也许我应该尝试更多地专注于理解函数式编程方法,并将性能问题留到真正需要它之前 :-)
    • @SteveMoseley 适应您的 Java 体验肯定是一个学习过程!请注意,瞬变仍然是不可变的,它们只有更快的assoc 和朋友版本。我绝对同意,最好先对 Clojure 习语和方法感到更熟悉,并不断提醒自己,许多这些感觉相当繁重的操作实际上并没有在内部进行太多。玩得开心! :)
    【解决方案2】:

    获得更多选择: 您可以使用 for 而不是 map

    (into {}
       (for [[key value] your-map]
             [key (do-stuff value)]))
    

    更快的方法是reduce-kv

    (reduce-kv 
       (fn [new-map key value] 
             (assoc new-map key (do-stuff value))) 
       {}
       your-map))
    

    当然你也可以使用简单的reduce

    (reduce (fn [m key]
              (update m key do-stuff))
       your-map
       (keys your-map))  
    

    【讨论】:

    • 哇 - 三种替代方法,太好了!我没有意识到'reduce'可以这样使用,甚至'for',它让我意识到我还有很长的路要走,我才能真正确信我知道我在用 Clojure 做什么
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-13
    • 2016-11-14
    相关资源
    最近更新 更多