2024 Clojure 状态调查! 中分享您的想法。

欢迎!请参阅 关于 页面了解更多关于如何使用本站的信息。

+19
Clojure
已关闭

许多人都在编写一个用于映射 HashMap 中值的函数

提议:添加 map-keysmap-values,它们:映射 HashMap 中的键和值。它们返回 HashMap 作为结果。

解决方案:使用函数 reduce-kv 或普通的 mapinto 是一个常见的解决方案,但它们很复杂,类型发生变化,这使得操作变得复杂且繁琐。

讨论:https://groups.google.com/forum/#!topic/clojure-dev/kkPYIl5qj0o

以以下备注关闭:Clojure 1.11.0-alpha2 添加了对 `update-keys` 和 `update-vals` 的支持 - 请参见 CLJ-1959 和 CLJ-2651 以获取更多详细信息

13 个回答

0

评论者:delihiros

map-keys 和 map-vals 的代码和测试

0

评论者:bronsa

我建议将这些函数命名为 update-valsupdate-keys 而不是 map-valsmap-keys

0

评论人:alexmiller

在这个问题上命名并不是关键——Rich 会给出他自己的看法。

关于这个补丁
- 删除了 :static 元数据,因为现在不再使用
- 需要添加文档字符串,应采用与其它Clojure文档字符串相同的风格。可以从map函数中获取灵感。
- 而不是声明定义,我们先推迟这些定义,直到需要它们所依赖的部分已经定义完毕。没有必要添加更多声明。

还有其他潜在的实现方式——这些应该得到实现,并在不同大小的输入范围内进行性能比较。除了当前的方法外,我还将调查

  • 使用reduce-kv和构造transient map。这允许map自我缩减(无需seq缓存),并避免创建条目再次拆分。
  • 使用transducers进行(into {} (map ...) m)

还应考虑以下因素
- 是否构建k/v矢量并将其转换为map,或者直接构建map(前者可能更快,不确定)
- 如果构建map,如何构造映射项(矢量和直接创建mapentry对象)
- 在map-keys中,当map生成新的重叠键时,是否有任何未解决的问题?
- 在现有核心代码中,哪里可以使用map-keys/map-vals?(我相当确定是有的)

0

评论者:delihiros

感谢您的评论:

我建议将这些函数命名为 update-valsupdate-keys 而不是 map-valsmap-keys
也许吧。但现在我把它命名为 map-**,稍后再选择:)

关于潜在的实施方案
我已经尝试了多种实施方案,并且当前的实施方案似乎是最快的。
您可以在这里看到: https://github.com/delihiros/performance

关于考虑因素
> 是否构建k/v矢量和到map的转换,还是直接构建map(前者可能更快,不确定)
> 是否可以在现有核心代码中的某些地方使用map-keys/map-vals(我相当确定是有的)
> 如果构建map,如何构建映射项(矢量和直接创建mapentry对象)
我将尽快检查这些问题。我还没有做。

在map-keys中,当map生成新的重叠键时,是否有任何未解决的问题?
我认为应该由后来应用的关键值覆盖。

0

评论人:nathanmarz

通过建立Specter,我对此进行了大量研究。以下是各种实现map-vals的方法的基准测试,包括使用Specter的情况。

代码: https://github.com/nathanmarz/specter/blob/4778500e0370fb211f47ebf4d69ca64366117b6c/scripts/benchmarks.clj#L87
结果: https://gist.github.com/nathanmarz/bf571c9ed86bfad09816e17b9b6e59e3

一些评论
- 构建和拆解MapEntry的实现效果较差。
- 对于大型映射应使用瞬态,但不是小型映射。
- 这个基准测试表明,在输出中保持映射类型的属性可以实现而无需牺牲性能(使用Specter或“空”的测试用例具有此属性)。

0

评论者:delihiros

我已经修改了实现。它应该比之前更快。

0

评论者:[email protected]

调用reduce-kv的实现不是懒实现的,因此应该在提出的补丁(map-mapper-v3.patch)中明确说明文档。也许最好用“map”而不是指明特定的具体类型“哈希表”。

0

评论者:bronsa

map->map操作本身也不能是懒实现的。即使一个实现使用了懒操作遍历原始映射,最终也会通过into {}来触发。

0

评论人:nathanmarz

- 对此持-1。Clojure旨在保持小的核心,将附加功能推入库。复合转换的问题领域,其中这个功能只是其中一小部分,已经被Specter充分解决。Specter的MAP-VALS和MAP-KEYS导航器此外还支持在转换过程中通过转换为特殊的NONE值来删除键值对。这极大地提高了实用性。

还值得注意的是,根据映射的类型,快速实现需要完全不同的方法。对于哈希表,使用transients的reduce-kv是最佳的,但对于数组映射,使用更高级的功能提供了大约60%的速度提升。请参阅Specter在此处的实现 https://github.com/nathanmarz/specter/blob/1.0.4/src/clj/com/rpl/specter/navs.cljc#L243

0

评论由:gshayban发表

Nathan,您在复合转换问题上提出的是一个 strawman。这不是一个要求具有过滤旋钮或条件行为的函数(Clojure历史上一直反对这些),有其他比 Specter 的更有效的途径。

关于快速实现:并不是每个函数都必须追求最高效的实现,特别是在增加分支和复杂性代价的情况下。性能的成本模型必须考虑到复杂性。

这是一项在许多代码库中反复出现的便利性请求。

我们是否想保留元数据?许多映射操作都会这样做。
我们是否想假设 IEditableCollection?

0

评论人:nathanmarz

性能对于这类一般数据结构操作函数至关重要。操作数据结构是你在语言中进行的最基本的事情之一,而且在性能敏感的代码中经常这样做。为了在性能敏感的代码中不得不放弃“漂亮”的函数而使用丑陋的专用代码,这是语言的失败。

map-vals/map-keys 是 Specter 用户和我过去几年学习到的丰富问题空间的一部分。Clojure 基本上没有触及这个领域,特别是当涉及到嵌套或递归数据结构时。在 Clojure 中添加函数,这些函数在语义和性能方面都远不如已存在于外部库中的函数,这似乎对我来说非常愚蠢。仅代表我个人意见。

0

评论者:delihiros

我同意 Nathan Martz 的观点,性能非常重要,但我仍然强烈认为这个函数应该以某种方式被导入到语言的核心部分。
人们经常使用这种转换,如果核心有快速实现,那将对我们所有人都有很大帮助。

0
参考资料:https://clojure.atlassian.net/browse/CLJ-1959 (由 delihiros 报告)
...