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。

"在做代码形式操作的时候,有时会希望有一个保持关键顺序的映射。Array map就是这样一种映射-" https://clojure.org/reference/data_structures#ArrayMaps
此外,我认为如果数组映射在语言中,它应该是有原因的。如果它不保留顺序,它只是常规映射的低效版本(但实际上它确实保留了顺序;是遍历器不保留)。
不仅仅是在后序遍历中,还有N个其他函数将映射作为输入并返回映射,但它们并没有保证返回有序数组映射,因此不能保留顺序,例如:conj(如果你超过8个键)、merge、assoc等。那些承诺返回相同顺序的数组映射的(而且当前实现中确实没有这么承诺)列表非常非常长。
你引用了Clojure.org官方文档中关于数组映射的部分,其中在同一段文字中也说:“注意,只有在未'修改'的情况下,数组映射才会保持排序顺序。后续的关联操作最终会导致它变成哈希映射。”
令人混淆的因素是实现中数组映射的一般自动提升。即使是用户定义的数组映射也会在被关联操作时自动提升。默认情况下,数组映射(可能通过常规哈希映射读取器字面量或提供8个键值的哈希映射构造函数构建),如果其大小变为>8,将自动提升到哈希映射。即使是用户定义的数组映射也不是免疫的;如果你保持计数不变,你可以在保留数组映射实现的同时更新现有键,但一旦增加一个新的条目,结构将提升到哈希映射(以提高效率)。

公平地说,假设使用身份行走,我预期输入将保持不变,并且不会返回哈希映射(这是我的假设)。实际上,'walk' 函数会修改数组映射,尽管实际上没有进行任何修改操作。

以下是 '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)))

如果对核心的 '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' 执行了一些操作来扩大结果映射。
by
感谢所有评论!walker 的补丁正是我所想的。

Array-map 根据定义似乎并不是非常有用或使用得非常多。但有序映射在某些罕见情况下是一个有用的抽象;也许应该将其添加到语言中。与数组映射不同,它们在伪修改下会保持其自然属性。
这里是顺序映射(和集合)的实现

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

此外,CLJ-1239的相关内容,它提出了基于协议的clojure.walk(我相信您可以通过扩展其Walkable协议到数组映射来获得所需的行为)

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