;; reducing function signature
whatever, input -> whatever
归约函数是您传递给 **reduce** 的那种函数 - 它是一个函数,它接收一个累积结果和一个新输入,并返回一个新的累积结果。
;; reducing function signature
whatever, input -> whatever
变换器(有时称为 xform 或 xf)是从一个归约函数到另一个归约函数的转换。
;; transducer signature
(whatever, input -> whatever) -> (whatever, input -> whatever)
Clojure 中包含的大多数序列函数都具有一个产生变换器的元数。此元数省略了输入集合;输入将由应用变换器的过程提供。注意:此减少的元数不是柯里化或偏应用。
例如
(filter odd?) ;; returns a transducer that filters odd
(map inc) ;; returns a mapping transducer for incrementing
(take 5) ;; returns a transducer that will take the first 5 values
变换器与普通的函数组合进行组合。变换器在决定是否以及调用其包装的变换器多少次之前执行其操作。组合变换器的推荐方法是使用现有的 **comp** 函数。
(def xf
(comp
(filter odd?)
(map inc)
(take 5)))
变换器 xf 是一个转换栈,将由一个过程应用于一系列输入元素。栈中的每个函数都在其包装的操作之前执行。转换器的组合从右到左运行,但构建一个从左到右运行的转换栈(在此示例中,过滤发生在映射之前)。
作为助记符,请记住 **comp** 中变换器函数的排序顺序与 **->>** 中序列转换的排序顺序相同。上面的转换等效于序列转换
(->> coll
(filter odd?)
(map inc)
(take 5))
以下函数在省略输入集合时会生成一个变换器:map cat mapcat filter remove take take-while take-nth drop drop-while replace partition-by partition-all keep keep-indexed map-indexed distinct interpose dedupe random-sample
应用变换器最常见的方法之一是使用 transduce 函数,它类似于标准的 reduce 函数。
(transduce xform f coll)
(transduce xform f init coll)
**transduce** 将立即(而不是延迟地)使用应用于归约函数 **f** 的变换器 **xform** 对 **coll** 进行归约,如果提供了 init 则使用它作为初始值,否则使用 (f)。f 提供了如何累积结果的知识,这发生在 reduce 的(可能是有状态的)上下文中。
(def xf (comp (filter odd?) (map inc)))
(transduce xf + (range 5))
;; => 6
(transduce xf + 100 (range 5))
;; => 106
组合的 xf 变换器将从左到右调用,最后调用归约函数 f。在最后一个示例中,输入值将被过滤,然后递增,最后求和。
要捕获将变换器应用于 coll 的过程,请使用 eduction 函数。它接受任意数量的 xform 和一个最终的 coll,并返回一个可归约/可迭代的变换器对 coll 中项目的应用。每次调用 reduce/iterator 时,都会执行这些应用。
(def iter (eduction xf (range 5)))
(reduce + 0 iter)
;; => 6
要从将变换器应用于输入集合的操作中创建一个序列,请使用 sequence
(sequence xf (range 1000))
生成的序列元素是增量计算的。这些序列将根据需要增量地使用输入并完全实现中间操作。此行为与延迟序列上的等效操作不同。
变换器具有以下形状(“…”中的自定义代码)。
(fn [rf]
(fn ([] ...)
([result] ...)
([result input] ...)))
许多核心序列函数(如 map、filter 等)接受特定于操作的参数(谓词、函数、计数等)并返回此形状的变换器,从而封闭这些参数。在某些情况下,例如 **cat**,核心函数是一个变换器函数,并且不接受 **rf**。
内部函数定义了 3 个用于不同目的的元数。
**Init**(元数 0) - 应该调用嵌套转换 **rf** 上的 init 元数,它最终将调用到变换过程。
**Step**(元数 2) - 这是一个标准的归约函数,但它预计会根据变换器中的需要调用 **rf** step 元数 0 次或多次。例如,filter 将根据谓词选择是否调用 **rf**。map 将始终准确调用它一次。cat 可能会根据输入多次调用它。
**Completion**(元数 1) - 一些过程不会结束,但对于那些结束的过程(如 **transduce**),completion 元数用于生成最终值和/或刷新状态。此元数必须准确调用 **rf** completion 元数一次。
**completion** 的一个示例用法是 **partition-all**,它必须在输入结束时刷新任何剩余的元素。completing 函数可用于通过添加默认 completion 元数将归约函数转换为变换器函数。
Clojure 有一种机制可以指定 reduce 的提前终止。
使用变换器的过程必须检查并在 step 函数返回已归约的值时停止(有关详细信息,请参阅创建可变换的过程)。此外,使用嵌套 reduce 的变换器 step 函数必须在遇到已归约的值时检查并传递已归约的值。(有关示例,请参阅 cat 的实现。)
一些变换器(例如 **take**、**partition-all** 等)在归约过程中需要状态。每次可变换的过程应用变换器时都会创建此状态。例如,考虑将一系列重复值折叠成单个值的 dedupe 变换器。此变换器必须记住前一个值才能确定是否应传递当前值。
(defn dedupe []
(fn [xf]
(let [prev (volatile! ::none)]
(fn
([] (xf))
([result] (xf result))
([result input]
(let [prior @prev]
(vreset! prev input)
(if (= prior input)
result
(xf result input))))))))
在 dedupe 中,**prev** 是一个有状态的容器,用于在归约期间存储前一个值。prev 值是为了性能而使用的易失性值,但它也可以是一个 atom。prev 值只有在变换过程开始时才会初始化(例如,在调用 **transduce** 时)。因此,有状态的交互包含在可变换过程的上下文中。
在 completion 步骤中,带有归约状态的变换器应在调用嵌套转换器的 completion 函数之前刷新状态,除非它之前已从嵌套步骤中看到一个已归约的值,在这种情况下,应丢弃挂起的状态。