【问题标题】:Find the elements of a LazySeq that have been realized找到一个已经实现的 LazySeq 的元素
【发布时间】:2013-12-03 05:15:06
【问题描述】:

我有一个在实现时创建的连接的 LazySeq。如果在尝试创建连接时发生异常,我想遍历所有已经在 LazySeq 中实现的连接并关闭它们。比如:

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

这并不完全有效,因为close-connections 将尝试再次实现连接。我只想关闭已经实现的连接,不实现额外的连接。有什么想法吗?

【问题讨论】:

    标签: clojure lazy-sequences


    【解决方案1】:

    代码:

    这会将先前实现的输入 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 所建议的那样。但是,正如我在我的 cmets 中对 Nathan 的回答所解释的那样,还必须确保不会在输入时无意中调用 seq,因为这会导致输入序列的先前未实现的片段被实现。这意味着 non-emptyempty? 之类的函数已被淘汰,因为它们是根据 seq 实现的。

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

    此外,虽然lazify 之类的函数对于取消分块序列很有用,但它们不会阻止其底层序列以分块方式实现;相反,它们使处理层(mapfilter 等)即使在原始输入序列被分块时也能以非分块方式运行。实际上,在实现这种“惰性”/“未分块”的 seq 与其底层的、可能是分块的 seq 实现之间根本没有任何联系。 (事实上​​,在输入 seq 的其他观察者在场的情况下,没有办法建立这样的连接;在没有其他观察者的情况下,它可以实现,但代价是使lazify 的编写变得更加乏味。)

    【讨论】:

    • 好答案。与我的回答不同,似乎在一般意义上起作用,而不仅仅是针对问题中列出的上下文。我希望可以避免looping 和instance?,但我想在这种情况下两者都非常必要。
    • 顺便说一句,你认为使用(instance? clojure.lang.IPending xs) 代替(instance? clojure.lang.LazySeq xs) 会更好吗?这样,如果有人决定制作自己的LazySeq 版本,它也可以工作(只要它实现IPending。不知道为什么有人会这样做,但如果你伸展得足够远,可能会有用例。
    • 好东西 - 非常感谢。
    • 干杯! @NathanDavis 你说得对,IPending 是“最小类型”,没有理由不使用它。
    【解决方案2】:

    更新:虽然此答案适用于原始问题中提供的上下文(在序列上运行 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))))
    

    但是,请注意,许多 Clo​​jure 的“惰性”构造并不是 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 和清理代码中都使用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))))
    

    【讨论】:

    • 这行不通,因为 (1) not-emptyempty? 在他们的参数上调用 seq,因此将实现惰性序列中先前未实现的部分; (2) lazify 返回一个 seq,它一次实现一个元素,但当然,它的底层 seq 将以任何通常的方式实现,所以“惰性”seq 只能实现一个链接,即使底层的分块 seq 已经实现了一个完整的初始块。
    • 试试(defn lazy-printer [n] (lazy-seq (when-not (zero? n) (println n) (cons n (lazy-printer (dec n))))))(take-realized (lazy-printer 10))。这应该不打印任何内容,但实际上会打印整数 10 到 1。
    • @Michał_Marczyk 感谢您提供的信息。我怀疑 lazify 并没有想要的那么懒(毕竟它确实调用了empty?,它必须检查是否有第一个,对吧?)。知道我的怀疑是正确的很好(但很烦人);-)。但是,在这种情况下,lazify 的主要目的是确保对rest 的调用将始终返回LazySeq(在正常情况下将始终完全实现该序列)。重要的是,如果意识到其中一个项目引发了异常,我们仍然可以获取没有引发异常的项目。我编辑了答案来解释这一点。
    • 顺便说一句,我认为这个问题会比实际上更容易回答。但是分块的序列一直让我感到困惑。当我发现一些完全有效的东西时,我很高兴。感谢您的反馈。
    • @Nathan 真棒信息。感谢您的回复。
    猜你喜欢
    • 2023-03-15
    • 1970-01-01
    • 2013-05-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-19
    • 1970-01-01
    • 2013-11-27
    相关资源
    最近更新 更多