序列对换能器有什么作用?

What does sequence do with a transducer?

关于sequence的两个相关问题:

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

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

  2. Clojure transducers page 说,关于 sequence 的用法,如上面的,

The resulting sequence elements are incrementally computed. These sequences will consume input incrementally as needed and fully realize intermediate operations. This behavior differs from the equivalent operations on lazy sequences.

对我来说,这听起来好像 sequence 不是 return 惰性序列,但 sequence 的文档字符串说 "When a transducer is supplied, returns a lazy sequence of applications of the transform to the items in coll(s), ....",实际上 (class (sequence xf (range 10))) returns clojure.lang.LazySeq。我想我不明白上面从 Clojure 转换器页面引用的最后一句话。

(sequence xform from)TransformerIterator[= 上创建 lazy-seq (RT.chunkIteratorSeq) 68=] 传递给 xformfrom。当请求下一个值时,xform(转换的组合)在 from 的下一个值上被调用。

This behavior differs from the equivalent operations on lazy sequences.

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

from 是一些不实现 IReduceInit[=68 的集合时,我会说 (into to xform from) 类似于 (into to (sequence xform from)) =].

into 内部使用 (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 个元素的映射,生成最终序列。

我觉得当前答案不够清楚,所以这里...

sequence 做 return 一个 LazySeq,但它是一个分块的,所以当你在 REPL 中使用它时,你通常会有它是 eager 的印象,因为你的集合可能会太小,分块会使它看起来很急切。我认为块大小有点动态,它不会总是大小完全相同的块,但通常它的大小似乎是 32。所以你的转换器将一次应用于输入集合 32 个元素,懒惰地。

这是一个简单的转换器,它只打印它减少的元素并且 return 将它们原封不动地打印出来:

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

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

(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)

不打印任何内容,并且每个都采用 returns 预期的结果集。我将 ;; => 用于 return 值,将 ;;> 用于打印输出。

好的,现在如果我们取第 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 使用转换器调用确实创建了一个惰性分块序列,其中元素仅在需要时处理(一次分块大小)。

这是关于什么的?:

The resulting sequence elements are incrementally computed. These sequences will consume input incrementally as needed and fully realize intermediate operations. This behavior differs from the equivalent operations on lazy sequences.

这是关于操作发生的顺序。使用 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 和换能器,它将处理元素,直到换能器结果具有块大小的元素计数。而在 lazy-seq 情况下,它将在每个级别分块处理,直到所有步骤都足以完成它们需要做的事情。

我的意思是:

(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

这里我修改了打印逻辑在同一行,这样就不用那么多了space。如果仔细观察,s 处理了输入范围的 66 个元素,而 l 只消耗了 32 个元素。

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

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