这是瞬变的正确用法吗?

Is this a correct usage of transients?

在演讲 "Bootstrapping Clojure at Groupon" by Tyler Jennings 中,从 25:14 到 28:24,他讨论了 separate 函数的两个实现,都使用瞬态:

(defn separate-fast-recur [pred coll]
  (loop [true-elements (transient [])
         false-elements (transient [])
         my-coll coll]
    (if (not (empty? my-coll))
      (let [curr (first my-coll)
            tail (rest my-coll)]
        (if (pred curr)
          (recur (conj! true-elements curr) false-elements tail)
          (recur true-elements (conj! false-elements curr) tail)))
      [(persistent! true-elements) (persistent! false-elements)])))

(defn separate-fast-doseq [pred coll]
  (let [true-elements (transient [])
        false-elements (transient [])]
    (doseq [curr coll]
      (if (pred curr)
        (conj! true-elements curr)
        (conj! false-elements curr)))
      [(persistent! true-elements) (persistent! false-elements)]))

(这些都是逐字复制的,包括第二行最后一行的单行缩进。)

他指出,在他使用的基准测试中,上面的第一个函数花费了 1.1 秒,而上面的第二个函数花费了 0.8 秒,并指出第二个因此优于第一个。但是,根据 Clojure documentation on transients:

Note in particular that transients are not designed to be bashed in-place. You must capture and use the return value in the next call.

因此在我看来,这个 separate-fast-doseq 函数是不正确的。但考虑到接下来谈话的性质,我很难相信这是不正确的。

这个 separate-fast-doseq 函数是否正确使用瞬态?为什么或为什么不?(如果没有,它破坏的例子是什么?)

由于您怀疑的原因,第二种实施方式不正确。 允许 瞬态集合为了效率而改变自身,但从不要求,因此这些 conj! 调用中的任何一个都可以return 具有不同身份的对象。如果发生这种情况,则通过丢弃 conj! 的结果,您粘贴的函数将无法正常运行。

但是,我无法提供它崩溃的例子。在 current implementation of Clojure 中,碰​​巧 conj! 总是在原地变异。请注意末尾的无条件 return this。因此,此函数将按预期运行。但是,它的正确性依赖于可能随时更改的实现细节。

有关确实中断的类似操作的示例,请尝试使用映射而不是向量:

(let [m (transient {})]
  (doseq [i (range 20)]
    (assoc! m i i))
  (count (persistent! m)))

8