Scala Transducers 和 Clojure Transducers 之间有哪些相同点和不同点?

What are the similarities and differences between Scala Transducers and Clojure Transducers?

Paul Chiusano and Rúnar Óli have written a fantastic book Functional programming in Scala。他们在其中提到了 Scala 社区中的一个 little-referenced 概念——Transducers。

在 Clojure 社区中 - Transducers get a little more press

我的问题是:Scala Transducers **(摘自 Functional Programming in Scala) 和 Clojure Transducers 之间有何相同点和不同点?**

假设:

我知道

  1. 换能器是their concept in Electrical Engineering

  2. 的常用说法
  3. 有个pre-existing概念in Computer Science called a Finite State Transducer

  4. 有先例in Biology and Psychology adopting the word transduction

  5. already a history of other technical books like SICP adopting the word Transducers

我不是特别熟悉 Scala 的换能器概念或该术语有多普遍,但根据您在上面发布的文本片段(以及我对换能器的了解),我可以说:

  • 他们很不一样
  • 它们的相似之处仅在于它们都涉及如何将一个集合转换为另一个集合

关于 Scala Transducers 我能说的:

从上面的定义看来,任何函数或可调用的签名大致与

一致
Stream[A] -> Stream[B]

例如,在这种情况下,工作流的映射函数将算作转换器。

就是这样;真的很简单。

Clojure 转换器:

一个Clojure transducer is a function that transforms one reducing function into another. A reducing function is one that can be used with reduce。也就是说,如果 Clojure 有签名,它就会有一个签名

(x, a) -> x

在英语中,给定一些起始集合 x,并且集合中的 "the next thing" a 被归约,我们的归约函数 returns "the next iteration of the collection being built".

因此,如果这是归约函数的签名,则转换器具有签名

((x, a) -> x) -> ((x, b) -> x)

将转换器添加到 Clojure 的原因是,通过添加或 core.async channels, Rich Hickey and friends found themselves reimplimenting all of the stanard collection functions to work with channels (map, filter, take, etc.). RH wondered if there wasn't a better way here, and went to work thinking about how to decomplect 来自手头集合类型机制的这些不同集合处理函数的逻辑。然而,我认为确切地解释传感器如何做到这一点超出了这个问题的范围,所以我会回到正题。但是,如果您有兴趣,可以很容易地找到和研究很多这方面的文献。

那么这些东西有什么关系呢?

显然,这些是非常不同的概念,但我是这样看待它们的:

虽然 Scala 转换器是 Streams 的集合处理函数(相对于其他 Scala 集合),但 Clojure 的转换器实际上是一种机制,用于统一跨 不同集合类型 [=54= 的集合处理函数的实现].因此,一种表达方式是,如果 Scala 具有 Clojure 的转换器概念,则 Scala 的转换器概念可以根据 Clojure 的转换器概念来实现,后者更多 abstract/general 处理函数可重用于多种集合类型。

Function Programming in Scala (FPiS) and Clojure's transducers are quite similar. They are a generalisation of the idea of having a "machine" (step function) to process the input stream into the output stream. FPiS's transducers are called Processes. Rich Hickey also uses the term process 一书中关于 Clojure 转换器的介绍性演讲中的流转换器。

起源

FPiS 传感器的设计基于Mealy machines。据说 Mealy 机器有:

transition function T : (S, I) -> S
output function     G : (S, I) -> O

这些功能可以融合在一起形成:

step: (S, I) -> (S, O)

这里很容易看出step函数对机器的当前状态和下一个输入项进行操作,从而产生机器的下一个状态和输出项。

FPiS 的其中一个组合器使用了这样一个阶跃函数:

trait Process[I, O] {
  ...
  def loop[S, I, O](z: S)(f: (I,S) => (O,S)): Process[I, O]
  ...
}

这个 loop 函数本质上是 Rickey 所说的 in this slide.

的种子左缩减

上下文不可知

两者都可以在许多不同的上下文中使用(例如列表、流、频道等)。

在 FPiS 传感器中,过程类型是:

trait Process[I, O]

它只知道输入元素和输出元素。

在 Clojure 中,情况类似。希基称之为 "fully decoupled".

作文

两种换能器均可组合

FPiS 使用 "pipe" 运算符

map(labelHeavy) |> filter(_.nonFood)

Clojure 使用 comp

(comp
  (filtering non-food?)
  (mapping label-heavy))

代表

在 Clojure 中:

reducer:    (whatever, input) -> whatever
transducer: reducer -> reducer

在 FPiS 中:

// The main type is
trait Process[I, O]

// Many combinators have the type
Process[I, O] ⇒ Process[I, O]

但是,FPiS 的表示不仅仅是一个隐藏的功能。这是一个 case-class(代数数据类型),有 3 个变体:Await、Emit 和 Halt。

case class Await[I,O](recv: Option[I] => Process[I,O])
case class Emit[I,O](head: O, tail: Process[I,O]
case class Halt[I,O]() extends Process[I,O]
  • Await 扮演 Clojure 中的 reducer->reducer 函数的角色。
  • Halt 在 Clojure 中扮演 reduced 的角色。
  • Clojure中Emit代替调用下一步函数

提前终止

两者都支持提前终止。 Clojure 使用一个名为 reduced 的特殊值来执行此操作,可以通过 reduced? 谓词对其进行测试。

FPiS 使用更静态类型的方法,进程可以处于 3 种状态之一:等待、发出或暂停。当一个 "step function" return 状态为 Halt 的进程时,处理函数知道要停止。

效率

在某些方面它们又很相似。两种类型的转换器都是需求驱动的,不会生成中间集合。但是,我认为 FPiS 的传感器在 pipelined/composed 时效率不高,因为内部表示大于 "just a stack of function calls" as Hickey puts it。不过,我只是在猜测 efficiency/performance。

查看 fs2(以前的 scalaz-stream),寻找基于 FPiS 传感器设计的性能可能更高的库。

示例

这是两个实现中 filter 的示例:

Clojure,from Hickey's talk slides

(defn filter
  ([pred]
    (fn [rf]
      (fn
        ([] (rf))
        ([result] (rf result))
        ([result input]
          (if (prod input)
            (rf result input)
            result)))))
  ([pred coll]
    (sequence (filter red) coll)))

在 FPiS 中,这是一种实现方式:

def filter[I](f: I ⇒ Boolean): Process[I, I] =
  await(i ⇒ if (f(i)) emit(i, filter(f))
            else filter(f))

如您所见,filter 是从 awaitemit.

等其他组合器构建而来的

安全

在实施 Clojure 转换器时,有许多地方必须小心。这似乎是一种有利于效率的设计权衡。然而,这种不利影响似乎主要影响图书馆生产商,而不是 end-users/consumers。

  • 如果传感器从嵌套的阶跃调用中获得 reduced 值,它绝不能再次调用该阶跃函数进行输入。
  • 需要状态的传感器必须创建唯一的状态并且不能被别名。
  • 所有阶跃函数都必须有一个不接受输入的 arity-1 变体。
  • 转换器的完成操作必须调用它的嵌套完成操作,恰好一次,并且return它returns。

FPiS 的传感器设计注重正确性和易用性。管道组合和 flatMap 操作确保完成操作及时发生并且错误得到适当处理。这些问题对传感器的实施者来说不是负担。也就是说,我认为该库可能不如 Clojure 库高效。

总结

Clojure 和 FPiS 转换器都具有:

  • 相似的起源
  • 在不同上下文中使用的能力(列表、流、频道、file/network io、数据库结果)
  • 需求驱动/提前终止
  • finalisation/completion(为了资源安全)
  • tasty:)

它们的基本表示有所不同。 Clojure 风格的传感器似乎有利于效率,而 FPiS 传感器有利于正确性和组合性。