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

时间:2016-09-19 05:39:11

标签: clojure 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的用法,如上所述,

  3.   

    逐步计算得到的序列元素。这些序列将根据需要逐步消耗输入并完全实现中间操作。此行为与惰性序列的等效操作不同。

    听起来好像sequence没有返回一个懒惰的序列,但sequence的文档字符串说"当提供一个传感器时,返回一个懒惰的应用程序序列转换为coll(s),...."中的项目,实际上(class (sequence xf (range 10)))返回clojure.lang.LazySeq。我想我不理解上面从Clojure传感器页面引用的最后一句话。

2 个答案:

答案 0 :(得分:1)

(sequence xform from)通过 xform TransformerIterator 创建 lazy-seq RT.chunkIteratorSeq )和传递。当请求下一个值时,将从中的下一个值调用 xform (转换组合)。

  

此行为与延迟序列的等效操作不同。

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

我会说(into to xform 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个元素的映射,生成最终序列。

答案 1 :(得分:0)

我发现当前答案不够清晰,所以去...

sequence确实返回了LazySeq,但它是一个分块的,因此,当您在REPL中使用它时,通常会感到它很渴望,因为您的收藏可能太小了,并且分块会使它看起来更热切。我认为数据块的大小有些动态,它不一定总是相同大小的数据块,但是通常看起来它的大小是32。因此,您的传感器将一次应用于输入集合32个元素,懒惰的。

这是一个简单的换能器,它仅打印缩小的元素并使其保持不变:

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

什么都不会打印,并且每次拍摄都会返回预期的结果集。我将;; =>用于返回值,并将;;>用于打印输出。

好吧,现在,如果我们采用第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和一个传感器,它将处理元素,直到传感器结果具有元素的块大小计数。在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

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

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

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