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

欢迎!有关如何操作的更多信息,请参阅关于页面。

0 投票
集合

在我看来,walker没有正确处理array-maps。它将它们转换为普通映射并丢失了顺序信息。

> (def x (array-map :a 1 :b 2 :c 3 :d 4 :e 5 :f 6 :g 7 :h 8 :i 9 :j 10))
#'x
> (class x)
clojure.lang.PersistentArrayMap
> x
{:e 5, :g 7, :c 3, :j 10, :h 8, :b 2, :d 4, :f 6, :i 9, :a 1}
> (def xw (clojure.walk/postwalk identity x))
#'xw
> (class xw)
clojure.lang.PersistentHashMap
> xw
{:e 5, :g 7, :c 3, :j 10, :h 8, :b 2, :d 4, :f 6, :i 9, :a 1}

1 答案

0 投票

映射是无序的,并且通常映射“修改”操作不保证顺序。因此,我认为这根本不是一个bug。

"在编辑代码形式时,有时需要保持键顺序的映射。数组映射就是这样一种映射 -"  https://clojure.org/reference/data_structures#ArrayMaps
此外,我认为如果数组映射存在于语言中,那么它存在一定是出于某种原因,而且如果它不保持顺序,那么它只是一个常规映射的低效版本(事实上它是保持顺序的;是不好的遍历方式导致不平顺)。
这不仅仅是对postwalk,还有N个其他接收映射作为输入并返回映射的函数,不保证返回数组和映射,因此不保留顺序,例如:conj(如果你超过8个键),merge,assoc等。那些承诺不会返回与数组映射相同顺序的列表(并且在当前实现中不会是这样)非常非常长。
您引用了Clojure.org官方文档中的数组映射部分,其中在您引用的同一段落中也提到:“注意,仅在未修改的情况下,数组映射才会保持排序顺序。后续的assoc操作最终会导致它“变成”哈希映射。”
让人感到困惑的是实现中普遍的数组映射自动提升。用户定义的数组映射在assoc时也会自动提升。默认情况下,数组映射(可能是通过正常的哈希映射读取字面量或提供8个键值的哈希映射构造函数创建的),当其大小变为>8时将自动提升到哈希映射。即使是用户定义的数组映射也不是无懈可击的;如果你保持计数不变,你可以在保持数组映射实现的同时更新现有键,但一旦你扩展了一个新条目,结构就提升为哈希映射(以提高效率)。

就公平而言,使用身份遍历,我预期输入将不会改变并且不会返回哈希映射(但这仅是我的假设)。按照实现,即使没有真正对它进行修改,walk也会改变数组映射

这就是`walk`中的实现所发生的情况,它会穿过`cond`中的条件直到它遇到随机收集实现

    (coll? form) (outer (into (empty form) (map inner form)))

`empty`创建一个空的数组映射,然后通过`conj`(实际上是使用`conj!`临时地)增长,最终达到数组映射的8个限制并升级到哈希映射。

一个简单的解决方案是为walk提供自定义情况,以检测数组映射并将它们保留原样。要完成此操作,您必须显式构建数组映射。

    (定义 walk
      [inner outer form]
      (cond
        (list? form) (outer (apply list (map inner form)))
        (instance? clojure.lang.IMapEntry form)
        (outer (clojure.lang.MapEntry/create (inner (key form)) (inner (val form)))))
        (seq? form) (outer (doall (map inner form)))
    
        (instance? clojure.lang.PersistentArrayMap form)
        (outer (apply array-map (reduce (fn [acc [k v]] (conj acc (inner k) (inner v))) [] form)))
    
        (instance? clojure.lang.IRecord form)
        (outer (reduce (fn [r x] (conj r (inner x))) form form))
        (coll? form) (outer (into (empty form) (map inner form)))
        :else (outer form))

如果您对 core.walk 进行猴改(monkey patch),它似乎可以正常工作

    clojure.walk=> (def x (array-map :a 1 :b 2 :c 3 :d 4 :e 5 :f 6 :g 7 :h 8 :i 9 :j 10))
    #'clojure.walk/x
    clojure.walk=> (def xw (clojure.walk/postwalk identity x))
    #'clojure.walk/xw
    clojure.walk=> xw
    {:a 1, :b 2, :c 3, :d 4, :e 5, :f 6, :g 7, :h 8, :i 9, :j 10}
    clojure.walk=> (= xw x)
    true
    clojure.walk=> (= (seq xw) (seq x))
    true
    clojure.walk=> (type xw)
    clojure.lang.PersistentArrayMap

然而,提升(promotion)仍然可能发生,例如如果 `outer` 对输出映射做了增长操作。
感谢大家的评论!对遍历器的修补正是我所想的。

Array-map看起来并不是很有用或按照定义来使用。但是有序映射在某些罕见情况下是一个有用的抽象;或许应该将它添加到语言中。与 array-maps 不同的是,它们会在伪变异(pseudo-mutation)下保留其性质。
以下是有序映射(和集)的实现

https://github.com/clj-commons/ordered

另外,CLJ-1239 也很有相关性,它提出了基于协议的 clojure.walk(我相信你可以通过扩展其 Walkable 协议到 array maps 来获得你期望的行为)

https://clojure.atlassian.net/browse/CLJ-1239
...