Clojure

变换器

变换器是可组合的算法转换。它们独立于其输入和输出源的上下文,并且仅根据单个元素指定转换的本质。因为变换器与输入或输出源解耦,所以它们可以在许多不同的过程中使用 - 集合、流、通道、可观察对象等。变换器可以直接组合,而无需了解输入或创建中间聚合。

另请参阅介绍性博客文章、此视频以及此 FAQ 部分关于变换器的良好用例

术语

归约函数是您传递给 **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))

使用变换器

变换器可以在许多上下文中使用(有关如何创建新的上下文,请参见下文)。

transduce

应用变换器最常见的方法之一是使用 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。在最后一个示例中,输入值将被过滤,然后递增,最后求和。

Nested transformations

eduction

要捕获将变换器应用于 coll 的过程,请使用 eduction 函数。它接受任意数量的 xform 和一个最终的 coll,并返回一个可归约/可迭代的变换器对 coll 中项目的应用。每次调用 reduce/iterator 时,都会执行这些应用。

(def iter (eduction xf (range 5)))
(reduce + 0 iter)
;; => 6

into

要将变换器应用于输入集合并构建一个新的输出集合,请使用 into(如果可能,它会有效地使用 reduce 和瞬态)。

(into [] xf (range 1000))

sequence

要从将变换器应用于输入集合的操作中创建一个序列,请使用 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 的提前终止。

  • reduced - 接收一个值并返回一个已归约的值,表示应停止归约。

  • reduced? - 如果该值是用reduced创建的,则返回 true。

  • deref 或 @ 可用于检索已归约内部的值。

使用变换器的过程必须检查并在 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 函数之前刷新状态,除非它之前已从嵌套步骤中看到一个已归约的值,在这种情况下,应丢弃挂起的状态。

创建可变换的过程

变换器旨在用于多种过程。可变换的过程定义为一系列步骤,其中每个步骤都接收一个输入。输入的来源特定于每个过程(来自集合、迭代器、流等)。类似地,该过程必须选择对每个步骤产生的输出执行的操作。

如果您有一个新的上下文来应用变换器,则需要了解一些一般规则。

  • 如果 step 函数返回一个已归约的值,则可变换的过程不得向 step 函数提供任何更多输入。在完成之前,必须使用 deref 解包已归约的值。

  • 完成过程必须准确地对最终累积值调用完成操作一次。

  • 变换过程必须封装对通过调用变换器返回的函数的引用 - 这些函数可能是状态化的,并且在跨线程使用时是不安全的。