为啥在 Clojure 的瞬态映射中插入 1000 000 个值会产生一个包含 8 个项目的映射?

Posted

技术标签:

【中文标题】为啥在 Clojure 的瞬态映射中插入 1000 000 个值会产生一个包含 8 个项目的映射?【英文标题】:Why inserting 1000 000 values in a transient map in Clojure yields a map with 8 items in it?为什么在 Clojure 的瞬态映射中插入 1000 000 个值会产生一个包含 8 个项目的映射? 【发布时间】:2015-06-23 10:55:03 【问题描述】:

如果我尝试对瞬态向量执行 1000 000 assoc!,我将得到一个包含 1000 000 个元素的向量

(count
  (let [m (transient [])]
    (dotimes [i 1000000]
      (assoc! m i i)) (persistent! m)))
; => 1000000

另一方面,如果我对地图做同样的事情,它只会有 8 个项目

(count
  (let [m (transient )]
    (dotimes [i 1000000]
      (assoc! m i i)) (persistent! m)))
; => 8

发生这种情况有什么原因吗?

【问题讨论】:

【参考方案1】:

瞬态数据类型的操作不保证它们将返回与传入的引用相同的引用。有时实现可能会决定在assoc! 之后返回一个新的(但仍然是瞬态的)映射而不是使用那个你过去了。

ClojureDocs page on assoc! 有一个 nice example 来解释这种行为:

;; The key concept to understand here is that transients are 
;; not meant to be `bashed in place`; always use the value 
;; returned by either assoc! or other functions that operate
;; on transients.

(defn merge2
  "An example implementation of `merge` using transients."
  [x y]
  (persistent! (reduce
                (fn [res [k v]] (assoc! res k v))
                (transient x)
                y)))

;; Why always use the return value, and not the original?  Because the return
;; value might be a different object than the original.  The implementation
;; of Clojure transients in some cases changes the internal representation
;; of a transient collection (e.g. when it reaches a certain size).  In such
;; cases, if you continue to try modifying the original object, the results
;; will be incorrect.

;; Think of transients like persistent collections in how you write code to
;; update them, except unlike persistent collections, the original collection
;; you passed in should be treated as having an undefined value.  Only the return
;; value is predictable.

我想重复最后一部分,因为它非常重要:您传入的原始集合应被视为具有未定义的值。只有返回值是可预测的。

这是您的代码的修改版本,可以按预期工作:

(count
  (let [m (transient )]
    (persistent!
      (reduce (fn [acc i] (assoc! acc i i))
              m (range 1000000)))))

附带说明,您总是得到 8 的原因是因为 Clojure 喜欢使用 clojure.lang.PersistentArrayMap(由数组支持的映射)来处理具有 8 个或更少元素的映射。超过 8 点后,它会切换到 clojure.lang.PersistentHashMap

user=> (type '1 a 2 a 3 a 4 a 5 a 6 a 7 a 8 a)
clojure.lang.PersistentArrayMap
user=> (type '1 a 2 a 3 a 4 a 5 a 6 a 7 a 8 a 9 a)
clojure.lang.PersistentHashMap

一旦超过 8 个条目,您的瞬态映射会将支持数据结构从对数组 (PersistentArrayMap) 切换到哈希表 (PersistentHashMap),此时 assoc! 返回一个新引用,而不仅仅是更新旧的。

【讨论】:

【参考方案2】:

最简单的解释来自Clojure documentation本身(强调我的):

瞬态支持一组并行的“改变”操作,名称相似,后跟! - assoc!, conj!等等。除了返回值本身是瞬态的之外,它们与它们的持久对应物做同样的事情。 请特别注意,瞬态并非旨在就地打击。您必须在下一次调用中捕获并使用返回值。

【讨论】:

以上是关于为啥在 Clojure 的瞬态映射中插入 1000 000 个值会产生一个包含 8 个项目的映射?的主要内容,如果未能解决你的问题,请参考以下文章

NHibernate 中的自引用实体给对象引用一个未保存的瞬态实例异常

HIbernate - 对象引用未保存的瞬态实例 - 在刷新之前保存瞬态实例

Grails Gorm:Object引用未保存的瞬态实例

CoreData 中的瞬态属性

对象引用未保存的瞬态实例 在刷新错误之前保存瞬态实例

对象引用未保存的瞬态实例:在刷新之前保存瞬态实例[重复]