在Clojure中,懒惰序列是否总是被分块?

18

我原本认为懒惰序列总是被分块的。

=> (take 1 (map #(do (print \.) %) (range)))
(................................0)

像预期的那样,会打印出32个点,因为由range返回的惰性序列是被分成32个元素一组的。然而,当我使用自己的函数get-rss-feeds代替range时,这个惰性序列不再被分组:

=> (take 1 (map #(do (print \.) %) (get-rss-feeds r)))
(."http://wholehealthsource.blogspot.com/feeds/posts/default")

只有一个点被打印出来,所以我猜测由get-rss-feeds返回的懒惰序列并没有分块。确实如此:

=> (chunked-seq? (seq (range)))
true

=> (chunked-seq? (seq (get-rss-feeds r)))
false

这是获取 get-rss-feeds 的源代码:

(defn get-rss-feeds
  "returns a lazy seq of urls of all feeds; takes an html-resource from the enlive library"
  [hr]
  (map #(:href (:attrs %))
       (filter #(rss-feed? (:type (:attrs %))) (html/select hr [:link])))
看起来,分块取决于怎样生成惰性序列。我查看了range函数的源代码,并且有迹象表明它是以“分块”的方式实现的。所以我有点困惑,希望有人可以澄清一下?
以下是我需要了解的原因。
我有以下代码:(get-rss-entry (get-rss-feeds h-res) url)get-rss-feeds的调用返回一个URL的惰性序列,我需要检查这个URL序列。
get-rss-entry的调用查找一个特定的条目(其:link字段与get-rss-entry的第二个参数相匹配)。它检查由get-rss-feeds返回的惰性序列。评估每个项目需要通过网络进行http请求来获取新的rss提要。为了最小化http请求的数量,重要的是逐个检查序列并在匹配后立即停止。
以下是代码:
(defn get-rss-entry
  [feeds url]
  (ffirst (drop-while empty? (map #(entry-with-url % url) feeds))))
entry-with-url返回一个懒惰的匹配序列,如果没有匹配,则返回一个空序列。
我测试了一下,对于每个反馈URL逐个评估,它似乎可以正确工作。但我担心有时会变得“卡顿”,并开始同时评估32个反馈。我知道有一种方法可以避免“卡顿”行为,就像这里讨论的那样,但在这种情况下甚至似乎不需要使用它。
我的懒惰序列的使用方式是否符合习惯?使用循环/递归是否更好?

1
看起来只有在使用clojure.core中的各种块函数或者你的序列实现了IChunkIChunkedSeq接口时,序列才会被“分块”。目前(在1.4.0版本中),这些都没有记录。 - noahlz
你使用的Clojure版本是哪个? - Arthur Ulfeldt
4个回答

11

你的担忧是正确的。如果feeds参数返回一个分块序列的集合,那么你的get-rss-entry确实会比必要的调用entry-with-url更多次。例如,如果feeds是一个向量,map将一次操作整个块。

在Fogus的《Clojure之乐》中,这个问题直接得到了解决,在第12章中定义了函数seq1

(defn seq1 [s]
  (lazy-seq
    (when-let [[x] (seq s)]
      (cons x (seq1 (rest s)))))) 

你可以在知道希望最大限度懒惰的位置使用它,在调用 entry-with-url 之前:

(defn get-rss-entry
  [feeds url]
  (ffirst (drop-while empty? (map #(entry-with-url % url) (seq1 feeds)))))

非常感谢。顺便说一句,我刚刚完成了这本书,它让我的Clojure编程水平提高了一个档次。迫不及待地等待更新版本的发布。 - Geo G
1
值得指出的是,必须在源代码中进行对seq1的取消分块调用。例如,如果您从分块序列上的map接收到一个惰性序列,那么您就没有办法了 - 无论您做什么,map都会向前查看。 - Thom

5

惰性序列并不总是分块的 - 这取决于它们如何生成。

例如,由此函数生成的惰性序列不是分块的:

(defn integers-from [n]
  (lazy-seq (cons n (do (print \.) (integers-from (inc n))))))

(take 3 (integers-from 3))
=> (..3 .4 5)

但是许多其他的Clojure内置函数出于性能原因会生成分块序列(例如range)。


2
非常重要的一点是,mapfilter都可能产生分块序列。混合副作用和惰性计算会导致微妙的错误。在这里,转换器可以提供帮助。 - event_jr

4
根据您上面提到的内容,依赖于分块的模糊性似乎不是一个明智的选择。在您确实需要避免分块的情况下,明确地 "取消分块" 也是明智的选择,因为这样,如果在其他某个时刻您的代码以一种将其分块化的方式发生了改变,事物就不会被打破。另外,如果您需要动作按顺序执行,则代理是一个很好的工具,你可以将下载函数发送到代理,然后它们将逐个运行,而且无论如何计算该函数,都只运行一次。在某些时候,您可能想对序列进行 pmap,然后即使取消分块也不起作用,但使用原子仍将正常工作。

2
请问您能否用示例代码来详细说明一下?您是指代理而不是原子吗? - noahlz
这里你是不是指代理而不是原子?因为提供给swap!的函数将会被重试。 - noisesmith
抱歉,我的手指出卖了我,按错了键...现在更正:s/atom/agent/g。 - Arthur Ulfeldt

0

我最近在Can I un-chunk lazy sequences to realize one element at a time?中讨论了这个问题,结论是如果你需要控制何时产生/消耗项目,则不应使用惰性序列。

对于处理,您可以使用转换器,在其中控制下一个项目的处理时间。

对于生成元素,理想的方法是reifyISeq。实际方法是使用lazy-seq,其中包含一个单一的cons调用,其余部分是递归调用。但请注意,这依赖于lazy-seq的实现细节。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接