我在这些场景中使用的经验法则(即,您希望单个输入序列产生多个输出序列的场景)是,在以下三个理想属性中,您通常只能拥有两个:
- 效率(仅遍历输入序列一次,因此不会保持领先)
- 懒惰(仅按需生成元素)
- 没有共享的可变状态
clojure.core 中的版本选择(1,3),但放弃了(2),一次生成了整个分区。Python 和 Haskell 都选择了 (1,2),尽管这不是很明显:Haskell 根本就没有可变状态吗?好吧,它对所有内容(不仅仅是序列)的惰性求值意味着所有表达式都是thunk,它们一开始是空白的,只有在需要它们的值时才被写入;span
正如您所说, 的实现span p xs'
在其两个输出序列中共享相同的 thunk ,因此无论哪个需要它,它首先将其“发送”到另一个序列的结果,从而在保持必要距离的距离处执行操作其他不错的属性。
正如您所指出的,您链接到的替代 Clojure 实现选择 (2,3)。
问题在于,对于partition-by
,下降 (1)或(2) 意味着您掌握了某个序列的头部:输入或输出之一。因此,如果您想要一个可以处理任意大输入的任意大分区的解决方案,您需要选择 (1,2)。在 Clojure 中有几种方法可以做到这一点:
- 采用 Python 方法:返回更像迭代器而不是 seq 的东西 - seqs 对非突变做出更强有力的保证,并承诺您可以安全地多次遍历它们,等等。如果您返回的不是 seq 的 seq,而是迭代器,然后使用来自任何一个迭代器的项目可以自由地改变或使其他迭代器无效。这保证了消费按顺序进行,并且可以释放内存。
- 采用 Haskell 方法:手动 thunk 一切,大量调用
delay
,并要求客户端force
尽可能频繁地调用以获取数据。这在 Clojure 中会更难看,并且会大大增加你的堆栈深度(在非平凡的输入上使用它可能会破坏堆栈),但理论上是可能的。
- 通过在输出 seq 之间协调一些可变数据对象来编写更多 Clojure 风格(但仍然很不寻常)的东西,每个输出 seq 都根据需要更新,当它们中的任何一个请求某些内容时。
我很确定这三种方法中的任何一种都是可能的,但老实说,它们都非常难,而且一点也不自然。Clojure 的序列抽象并不容易生成您想要的数据结构。我的建议是,如果您需要这样的东西并且分区可能太大而无法舒适地容纳,您只需接受稍微不同的格式并自己做更多的簿记:通过不产生多个来避免 (1,2,3) 困境完全输出序列!
因此,您可以接受一种稍微丑陋的格式,而不是((2 4 6 8) (1 3 5) (10 12) (7))
像. 这既不难生产也不难消费,虽然写出来有点冗长乏味。(partition-by even? [2 4 6 8 1 3 5 10 12 7])
([::key true] 2 4 6 8 [::key false] 1 3 5 [::key true] 10 12 [::key false] 7)
这是生产函数的一种合理实现:
(defn lazy-partition-by [f coll]
(lazy-seq
(when (seq coll)
(let [x (first coll)
k (f x)]
(list* [::key k] x
((fn part [k xs]
(lazy-seq
(when (seq xs)
(let [x (first xs)
k' (f x)]
(if (= k k')
(cons x (part k (rest xs)))
(list* [::key k'] x (part k' (rest xs))))))))
k (rest coll)))))))
下面是如何使用它,首先定义一个reduce-grouped
隐藏分组格式细节的泛型,然后是一个示例函数count-partition-sizes
来输出每个分区的键和大小,而不在内存中保留任何序列:
(defn reduce-grouped [f init groups]
(loop [k nil, acc init, coll groups]
(if (empty? coll)
acc
(if (and (coll? (first coll)) (= ::key (ffirst coll)))
(recur (second (first coll)) acc (rest coll))
(recur k (f k acc (first coll)) (rest coll))))))
(defn count-partition-sizes [f coll]
(reduce-grouped (fn [k acc _]
(if (and (seq acc) (= k (first (peek acc))))
(conj (pop acc) (update-in (peek acc) [1] inc))
(conj acc [k 1])))
[] (lazy-partition-by f coll)))
user> (lazy-partition-by even? [2 4 6 8 1 3 5 10 12 7])
([:user/key true] 2 4 6 8 [:user/key false] 1 3 5 [:user/key true] 10 12 [:user/key false] 7)
user> (count-partition-sizes even? [2 4 6 8 1 3 5 10 12 7])
[[true 4] [false 3] [true 2] [false 1]]
编辑:再看一遍,我真的不相信 myreduce-grouped
比 有用得多(reduce f init (map g xs))
,因为它并没有真正为您提供任何关于密钥何时更改的明确指示。因此,如果您确实需要知道组何时发生更改,您将需要更智能的抽象,或者使用我的原始lazy-partition-by
内容,而没有任何“聪明”的包装。