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

欢迎!有关本网站的工作原理,请参阅关于 页面以获取更多信息。

0
集合

在我看来,walker没有正确处理array-maps。它将它们转换为常规的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

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

"在进行代码形式操作时,通常希望有一个保持键顺序的map。这个数组map就是这样一种map..."  https://clojure.org/reference/data_structures#ArrayMaps
此外,我认为如果数组映射存在于语言中,那么它肯定有其存在的理由,并且如果它不保持顺序,它仅仅是一个有效映射的无效版本(实际上它确实保留了顺序;问题在于遍历器)。
不仅是在后遍历,还有其他N个输入映射并返回映射的函数都不会保证返回有序的数组映射,因此不保持顺序,例如:conj(当你超过8个键时),merge,assoc等。那些承诺返回与当前实现相同的顺序的数组映射列表非常非常长。
你引用了Clojure.org官方文档中的数组映射说明,其中也提到了你所引用的同一段话:“请注意,当没有经过修改时,数组映射才会保持排序顺序。后续的关联操作最终会导致它‘变成’一个哈希映射。”
混淆因子在于实现中数组映射的一般自提升。即使是用户定义的数组映射在关联操作中也会自提升。默认情况下,数组映射(可能是通过普通的哈希映射读取器字面量或包含 8 个键值的哈希映射构造函数构建),如果在大小变化到 > 8 时,将自动提升为哈希映射。即使是用户定义的数组映射也无法摆脱这种影响;如果保持计数不变,则可以在保留数组映射实现的同时更新现有键,但一旦增长一个新的条目,该结构就会提升为哈希表(为了效率)。

公平来说,用恒等性进行遍历,我预期输入会保持不变,并且不会返回哈希映射(尽管这是我的假设)。

这就是在 `walk` 实现中发生的情况,它穿过 `cond` 中的条件,直到遇到通用的集合实现。

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

`empty` 创建一个空数组映射,然后通过 `conj`(实际上是暂时通过 `conj!`)来进行扩展,最终达到数组映射的 8 个限制并及时提升到哈希表。

一个简单的解决方法是提供一个自定义的 `walk` 情况,用于检测数组映射并将它们保持不变。要这样做,必须明确地构造数组映射。

    (defn 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 进行篡改,这似乎可以工作。

    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

然而,提升仍然可能发生,例如如果 `outer` 对生成的映射进行了某些操作。
by
感谢所有的评论!对遍历器的补丁正是我所想的。

Array-map似乎并不非常有用或按照定义使用。但是关联数组在某些罕见情况下是一个有用的抽象,也许应该将其添加到语言中。与array-maps不同,它们可以在伪变异中保持其本质。
{.Empty} by
以下是关联数组(和集合)的实现

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

还有一个相关的问题是CLJ-1239,它提出了一个基于协议的clojure.walk(我相信您可以通过扩展其Walkable协议到array maps来获得您期望的行为)

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