找到已经实现的LazySeq的元素

时间:2013-12-03 05:15:07

标签: clojure lazy-sequences

我有一个在实现时创建的LazySeq连接。如果在尝试创建连接时发生异常,我想迭代已经在LazySeq中实现的所有连接并关闭它们。类似的东西:

(try  
  (dorun connections)
  (catch ConnectException (close-connections connections)))

虽然close-connections将尝试再次实现连接,但这并不是很有效。我只想关闭已实现的连接,而不是实现其他连接。这样做有什么想法吗?

2 个答案:

答案 0 :(得分:5)

代码:

这将先前实现的输入seq的初始片段作为向量返回:

(defn take-realized [xs]
  (letfn [(lazy-seq? [xs]
            (instance? clojure.lang.LazySeq xs))]
    (loop [xs  xs
           out []]
      (if (or (and (lazy-seq? xs) (not (realized? xs)))
              (and (not (lazy-seq? xs)) (empty? xs)))
        out
        (recur (rest xs) (conj out (first xs)))))))

在REPL进行测试:

(defn lazy-printer [n]
  (lazy-seq
   (when-not (zero? n)
     (println n)
     (cons n (lazy-printer (dec n))))))

(take-realized (lazy-printer 10))
;= []

(take-realized (let [xs (lazy-printer 10)] (dorun (take 1 xs)) xs))
;=> 10
;= [10]

;; range returns a lazy seq...
(take-realized (range 20))
;= []

;; ...wrapping a chunked seq
(take-realized (seq (range 40)))
;= [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
;   17 18 19 20 21 22 23 24 25 26 27 28 29 30 31]

;; NB. *each* chunk of range gets its own LazySeq wrapper,
;; so that it is possible to work with infinite (or simply huge) ranges

(使用;=>表示打印输出。)

讨论:

按照内森的建议,

realized?确实是要走的路。但是,正如我在对Nathan的答案的评论中所解释的那样,还必须确保一个人不会无意中在一个人的输入上调用seq,因为这会导致输入序列的先前未实现的片段被实现。这意味着non-emptyempty?等功能已经用完,因为它们是以seq实现的。

(事实上,根本不可能在没有意识到的情况下判断懒惰的seq是否为空。)

此外,虽然像lazify这样的函数对非拆分序列很有用,但它们并不能阻止它们的底层序列以分块的方式实现;相反,它们使得处理层(mapfilter等)能够以未分块的方式运行,即使它们的原始输入序列是分块的。实际上,在实现这样的“lazified”/“unchunked”seq与其底层的,可能是chunked seq之间实际上没有任何联系。 (事实上​​,没有办法在输入seq的其他观察者面前建立这样的连接;没有其他观察者,它可以完成,但只是以使lazify编写相当繁琐为代价。 )

答案 1 :(得分:3)

更新:虽然此答案适用于原始问题中显示的上下文(在序列上运行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导致nilLazySeq。换句话说,它运行良好,足以运行下面的示例,但YMMV。

现在,如果我们在dorun和清理代码中使用相同的“lazified”序列,我们将能够使用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))))