在演讲"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:
特别注意,瞬态并非设计为就地瞄准。您必须在下次调用中捕获并使用返回值。
因此在我看来这个separate-fast-doseq
函数是不正确的。但考虑到其余话题的性质,我很难相信这是不正确的。
此separate-fast-doseq
功能是否正确使用了瞬变?为什么或为什么不呢?(如果不是,它的例子是什么?)
答案 0 :(得分:7)
第二种实现方式不正确,因为您怀疑的原因。一个瞬态集合允许为了提高效率而改变自身,但它永远不会需要,因此这些conj!
调用中的任何一个都可能返回一个不同的对象身份。如果发生这种情况,那么通过放弃conj!
的结果,您粘贴的功能将表现不正确。
但是,我不能提供一个破坏的例子。在current implementation of Clojure中,conj!
确实总是在适当的位置发生变异。请注意最后的无条件return this
。因此,此函数将按预期运行。但是,它依赖于实现细节的正确性,这些细节可能随时发生变化。
对于类似操作的示例,请尝试使用地图而不是矢量:
(let [m (transient {})]
(doseq [i (range 20)]
(assoc! m i i))
(count (persistent! m)))
8