3

关于的两个相关问题sequence

给定一个换能器,例如(def xf (comp (filter odd?) (map inc)))

  1. (into [] xf (range 10))or(into () xf (range 10))和之间有什么关系(sequence xf (range 10))?只是没有可以用作第二个参数的惰性序列的语法into,所以我们需要一个单独的函数sequence来实现这个目的吗?(我知道它sequence还有另一种非转换器用途,将集合强制转换为一种或另一种序列。)

  2. Clojure 转换器页面说,关于sequence上面那个的使用,

生成的序列元素是增量计算的。这些序列会根据需要增量消耗输入,完全实现中间操作。此行为不同于惰性序列上的等效操作。

对我来说,这听起来好像sequence没有返回一个惰性序列,但是文档字符串sequence说“当提供了一个转换器时,返回一个惰性序列的转换应用程序到 coll(s),....中的项目”,事实上(class (sequence xf (range 10)))回报clojure.lang.LazySeq。我想我不明白上面从 Clojure 转换器页面引用的最后一句话。

4

2 回答 2

1

我没有找到足够清楚的当前答案,所以这里......

sequence确实返回了一个 LazySeq,但它是一个分块的,所以当你在 REPL 中使用它时,你通常会觉得它是渴望的,因为你的集合可能太小了,而分块会让它看起来很渴望. 我认为块大小有点动态,它并不总是完全相同大小的块,但通常它似乎大小为 32。所以你的转换器将一次应用于输入集合 32 个元素,懒洋洋。

这是一个简单的转换器,它只打印它减少的元素并原封不动地返回它们:

(defn printer
  [xf]
  (fn
    ([] (xf))
    ([result] (xf result))
    ([result input]
     (println input)
     (xf result input))))

如果我们用它创建一个s包含 100 个元素的序列:

(def s
  (sequence
   printer
   (range 100)))
;;> 0

我们看到它打印0,但没有别的。在调用 时sequence,第一个元素将从 中消耗(range 100),并将传递给xf要转换的链,在我们的例子中只是打印它。因此,除了第一个元素外,没有其他元素被消耗。

现在,如果我们从 中获取一个元素s

(take 1 s)
;;> 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
;;> 32

我们看到它打印了前 32 个元素。这是 Clojure 中分块惰性序列的正常行为。您可以将其视为半惰性,因为它一次消耗块大小的元素,而不是一次消耗 1 个。

现在,如果我们尝试从 1 到 32 中获取任何元素,则不会打印任何其他内容,因为前 32 个元素已被处理:

(take 1 s)
;; => (0)
(take 10 s)
;; => (0 1 2 3 4 5 6 7 8 9)
(take 24 s)
;; => (0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23)
(take 32 s)
;; => (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)

没有任何东西被打印出来,每次拍摄都返回预期的结果集。我;; =>用于返回值和;;>打印输出。

好的,现在如果我们采用第 33 个元素,我们希望看到下一个 32 个元素的块被打印:

(take 33 s)
;;> 33
;;> 34
;;> 35
;;> 36
;;> 37
;;> 38
;;> 39
;;> 40
;;> 41
;;> 42
;;> 43
;;> 44
;;> 45
;;> 46
;;> 47
;;> 48
;;> 49
;;> 50
;;> 51
;;> 52
;;> 53
;;> 54
;;> 55
;;> 56
;;> 57
;;> 58
;;> 59
;;> 60
;;> 61
;;> 62
;;> 63
;;> 64

惊人的!所以再一次,我们看到只取了接下来的 32 个,这使我们现在总共处理了 64 个元素。

好吧,这表明sequence使用转换器调用实际上会创建一个惰性分块序列,其中元素只会在需要时被处理(一次块大小)。

那么这是关于什么的?:

生成的序列元素是增量计算的。这些序列会根据需要增量消耗输入,完全实现中间操作。此行为不同于惰性序列上的等效操作。

这是关于操作发生的顺序。带sequence和换能器:

(sequence (comp A B C) coll)

块中的每个元素都会让它们通过: A -> B -> C,所以你得到:

A(e1) -> B(e1) -> C(e1)
A(e2) -> B(e2) -> C(e2)
...
A(e32) -> B(e32) -> C(e32)

而对于普通的惰性序列,例如:

(->> coll A B C)

将首先让所有分块元素通过 A,然后让它们都通过 B,然后通过 C:

A(e1)
A(e2)
...
A(e32)
|
B(e1)
B(e2)
...
B(e32)
|
C(e1)
C(e2)
...
C(e32)

这需要在每个步骤之间进行中间收集,因为必须将 A 的结果收集到一个集合中,然后循环并应用 B 等。

我们可以在前面的示例中看到这一点:

(def s
  (sequence
   (comp (filter odd?)
         printer
         (map vector)
         printer)
   (range 10)))

(take 1 s)
;;> 1
;;> [1]
;;> 3
;;> [3]
;;> 5
;;> [5]
;;> 7
;;> [7]
;;> 9
;;> [9]


(def l
  (->> (range 10)
       (filter odd?)
       (map #(do (println %) %))
       (map vector)
       (map #(do (println %) %))))

(take 1 l)
;;> 1
;;> 3
;;> 5
;;> 7
;;> 9
;;> [1]
;;> [3]
;;> [5]
;;> [7]
;;> [9]

看看第一个会怎样,filter -> vector -> filter -> vector, etc.而第二个会怎样filter all -> vector all。好吧,这就是文档中引用的意思。

现在还有一件事,两者之间的分块应用方式也有所不同。使用sequence和转换器,它将处理元素,直到转换器结果具有块大小的元素计数。而在惰性序列的情况下,它将在每个级别上分块处理,直到所有步骤都有足够的时间来完成他们需要做的事情。

这就是我的意思:

(def s
  (sequence
   (comp printer
         (filter odd?))
   (range 100)))

(take 1 s)
;;> 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65

(def l
  (->> (range 100)
       (map #(do (print % "") %))
       (filter odd?)))

(take 1 l)
;;> 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

在这里,我将打印逻辑修改为在同一行,因此不会占用太多空间。如果仔细观察,s处理了输入范围的 66 个元素,而l只消耗了 32 个元素。

原因就是我上面说的。使用sequence,我们将继续获取块,直到我们获得块大小的结果。在这种情况下,块大小为 32,并且由于我们过滤odd?,我们需要两个块才能达到 32 个结果。

使用lazy-seq,它不会尝试获取第一块结果,而是从输入中获取足够多的块来满足逻辑,在这种情况下,我们只需要从输入中获取 32 个元素中的一块即可找到单奇数取。

于 2020-04-05T05:35:03.553 回答
1

(sequence xform from)在xformfrom传递到的TransformerIterator上创建惰性序列(RT.chunkIteratorSeq) 。当请求下一个值时,会从from中的下一个值调用xform (转换组合) 。

此行为不同于惰性序列上的等效操作。

惰性序列上的等效操作是什么?以你的 xf 为例,应用filter odd?(range 10),产生中间惰性序列,并应用map inc到中间惰性序列,产生最终的惰性序列作为结果。

我会说这(into to xform from)类似于from(into to (sequence xform from))是一些不实现IReduceInit的集合。

进入内部使用(transduce xform conj to from),它的作用与(reduce (xform conj) to from)最后clojure.core.protocols/coll-reduce相同:

(into [] (sequence xf (range 10)))
;[2 4 6 8 10]
(into [] xf (range 10))
;[2 4 6 8 10]
(transduce xf conj [] (range 10))
;[2 4 6 8 10]
(reduce (xf conj) [] (range 10))
;[2 4 6 8 10]

我将您的换能器修改为:

(defn hof-pr
     "Prints char c on each invocation of function f within higher order function"
 ([hof f c]
   (hof (fn [e] (print c) (f e))))
 ([hof f c coll]
   (hof (fn [e] (print c) (f e)) coll)))
(def map-inc-pr (partial hof-pr map inc \m))
(def filter-odd-pr (partial hof-pr filter odd? \f))
(def xf (comp (filter-odd-pr) (map-inc-pr)))

以便它在每个转换步骤中打印出字符。

在 REPL 中创建s1如下:

(def s1 (into [] xf (range 10)))
ffmffmffmffmffm

s1被热切地评估(打印 f 用于过滤和 m 用于映射)。再次请求s1时不进行评估:

s1
[2 4 6 8 10]

让我们创建s2

(def s2 (sequence xf (range 10)))
ffm 

仅评估s2中的第一项。下一个项目将在要求时进行评估:

s2
ffmffmffmffm(2 4 6 8 10)

此外,创建 s3,旧方式:

(def s3 (map-inc-pr (filter-odd-pr (range 10))))
s3
ffffffffffmmmmm(2 4 6 8 10)

如您所见,定义s3时没有评估。当请求s3时,应用过滤 10 个元素,然后应用剩余 5 个元素的映射,产生最终序列。

于 2016-09-21T13:23:58.500 回答