我发现 split-with 实际上是按 [(take-while pred coll) (drop-while pred coll)] 的方式实现的,这意味着对于每个满足条件的元素,它将重复应用谓词进行冗余的工作量。
以下代码模拟了一个昂贵的谓词,它花费 800+ 毫秒并打印 0 1 2 3 0 1 2 3
(time
(mapv doall
(let [xs (range 10)
pred #(do (println %) (Thread/sleep 100) (< % 3))]
(split-with pred xs))))
为什么不能更高效地实现它以避免这些冗余评估呢?
这是我初次尝试的实现——请注意,创建 drops
的单个元素会导致 takes
全部实现,但反之则不然。
(defn split-with*
"Like `split-with` but evaluates `pred` at most once on each element"
[pred coll]
(let [takes (take-while pred coll)
drops (lazy-seq (drop (count takes) coll))]
[takes drops]))
附加上下文
这将对当前涉及谓词的副作用并依赖其运行两次的代码造成破坏性变更。
但这可能是一个坏主意——例如,以下有副作用的代码违反了理性假设,即 split-with
实际上将集合拆分为两个部分。
由于惰性,结果还取决于哪个序列先实现。
(let [xs (range 10)
counter (atom 1)
pred (fn [x] (< x (swap! counter + 1/2)))]
(split-with pred xs))
;; => [(0 1 2) (7 8 9)]