更新:虽然此答案适用于原始问题中提供的上下文(在序列上运行 doall,并确定在出现异常时实现了哪些),但它包含几个缺陷并且不适合问题标题建议的一般用途。然而,它确实提供了一个理论(但有缺陷)基础,可能有助于理解Michał Marczyk's answer。如果您在理解该答案时遇到困难,则此答案可能会通过将事情进一步分解来提供帮助。它还说明了您可能遇到的几个陷阱。但除此之外,请忽略这个答案。
LazySeq 实现了IPending,所以理论上这应该像迭代连续的尾序列一样简单,直到realized? 返回 false:
(defn successive-tails [s]
(take-while not-empty
(iterate rest s)))
(defn take-realized [s]
(map first
(take-while realized?
(successive-tails s))))
现在,如果你真的有一个 100% LazySeq 从开始到结束,就是这样 -- take-realized 将返回 s 已经实现的项目。
编辑:好吧,不是真的。这将用于确定在引发异常之前实现了哪些项目。然而,正如 Michal Marcyzk 所指出的,它会导致序列中的每个项目都在其他上下文中实现。
然后您可以像这样编写清理逻辑:
(try
(dorun connections) ; or doall
(catch ConnectException (close-connections (take-realized connections))))
但是,请注意,许多 Clojure 的“惰性”构造并不是 100% 惰性的。例如,range 将返回一个LazySeq,但如果你开始rest 向下它,它会变成一个ChunkedCons。不幸的是,ChunkedCons 没有实现IPending,并且在其中调用realized? 会抛出异常。为了解决这个问题,我们可以使用lazy-seq 显式构建一个LazySeq,对于任何序列保持LazySeq:
(defn lazify [s]
(if (empty? s)
nil
(lazy-seq (cons (first s) (lazify (rest s))))))
编辑: 正如 Michał Marczyk 在评论中指出的那样,lazify 确实不保证底层序列被延迟消耗。事实上,它可能会实现以前未实现的项目(但似乎只在第一次通过时抛出异常)。它的唯一目的是保证调用rest 会导致nil 或LazySeq。换句话说,它运行得很好,可以运行下面的示例,但是 YMMV。
现在,如果我们在dorun 和清理代码中都使用same“惰性”序列,我们将能够使用take-realize。这是一个示例,说明如何构建一个表达式,如果在实现时发生异常,该表达式将返回部分序列(失败之前的部分):
(let [v (for [i (lazify (range 100))]
(if (= i 10)
(throw (new RuntimeException "Boo!"))
i))]
(try
(doall v)
(catch Exception _ (take-realized v))))