2024年Clojure调查问卷 中分享您的想法!

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

0
集合

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

> (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
此外,我认为如果语言中有array-map,那么它应该有存在的理由。如果它不保留顺序,那它就是一个效率低下的常规map版本(但实际上它是保留订单的;没有保留的是walker)。
不仅仅是postwalk,还有N个其他将map作为输入并返回map的函数,它们并不保证返回保持顺序的array-map,因此不保留顺序,例如:conj(如果你有超过8个键)、merge、assoc等。关于那些承诺不返回相同顺序的array-map的内容非常非常长。
您引用了Clojure.org官方文档中的array-map部分,其中在您引用的同一段落中还说:"请注意,仅当未修改时,array-map才能维持排序顺序。随后的assoc-ing最终会使它变为hash-map。"
混淆的因素是实施中array map的普遍自动提升。即使用户定义的array map在assoc时也会自动提升。默认情况下,array map(可能通过正常的hash map reader literals构建或通过hash map构造函数提供8个键值对构建),大小在超过8时将自动提升为hash map。甚至用户定义的array map也不是免疫的;如果你保持计数不变,你可以更新现有键同时保留array map实现,但一旦你增长一个新条目,该结构就会提升为hashmap(为了效率)。

诚实地讲,使用identity进行遍历,我预期输入保持不变并且不会返回hashmap(这是我的一种假设)。按照实现,walk会更改array map,尽管实际上并没有真正对它进行修改。

这是`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 进行 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

然而,提升仍然是可能的,例如,如果 `outer` 对生成的映射进行了某些操作以扩展其大小。
by
感谢所有的评论!walker 的补丁正是我所想的。

数组映射似乎没有很大的用途或用法,但有序映射在某些罕见情况下是有用的抽象;可能应该将其添加到语言中。与数组映射不同,它们会在伪变异下保持其本质。
by
以下是有序映射(和集合)的实施实例

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

还有 CLJ-1239,它提出了基于协议的 clojure.walk(我相信你可以通过扩展其 Walkable 协议到数组映射来获得你想要的行为)

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