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
我还认为,如果语言中有array-map,那它肯定有存在的理由。如果它不保持顺序,那么它只是一个效率低下的常规map版本(但它确实保持顺序;问题在于walker没有保持顺序)。
by
这不仅仅是postwalk,还有许多其他以map为输入并返回map的N个函数不保证返回array-map,因此不保持顺序,例如:conj(如果你超过8个键)、merge、assoc等。那些承诺不返回同一顺序的array-map的列表非常非常长(并且当前实现也是如此)。
by
您引用了Clojure.org官方文档中对array maps的描述,其中在相同段落中也提到:“请注意,只有在不经过'modification'的情况下,array map才能保持排序顺序。随后的assoc操作最终会导致它'变成'哈希表。”
by
混淆的因素是实现中普遍的array map自动提升。甚至用户定义的array map在assoc操作时也会自动提升。默认情况下,array-maps(可能通过正常的hash-map阅读器文字或hash-map构造函数提供的8个键值创建),如果其大小变为> 8,将自动提升为hash maps。即使是用户定义的array map也不是免疫的;如果您保持计数不变,您可以在保留array-map实现的情况下更新现有键,但只要增加一个新条目,结构就会提升为hashmap(以提高效率)。

为了公平起见,使用identity进行遍历,我预计输入将保持不变,并且不会返回hashmap(这是我的假设)。按照当前实现,walk实际更改了array map,尽管实际上并没有对其进行修改。

这就是`walk`实现中的情况,它会在`cond`的条件中下陷,直到遇到通用的集合实现。

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

`empty`创建一个空的array map,然后通过`conj`(实际上是暂时的通过`conj!`)扩展,最终达到array map的8个限制并提升到hashmap。

一种简单的解决方案是提供一个定制的用于“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)))

如果您对核心的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
感谢所有评论!对walker的补丁正是我所希望的那样。

Array-map似乎没有很有用,也没有按照所定义的来使用。但有序映射在一些罕见情况下是有用的抽象,也许应该将其添加到语言中。与array-maps不同,它们会在近似变异下保持其性质。
by
这是有序映射(和集合)的实现

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

此外,CLJ-1239也与此相关,该协议提出了基于协议的clojure.walk(我相信您可以通过扩大其Walkable协议到数组映射来实现您想要的行为)

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