Paul Chiusano和Rúnar Óli写了一本很棒的书Scala函数式编程。他们在书中提到了一个在Scala社区中鲜有人谈及的概念 - Transducers。
在Clojure社区中,转换器受到了一些 更多的 关注。我的问题是:Scala Transducers(来自Scala函数式编程书籍)和Clojure Transducers之间有哪些相似之处和不同之处? 假设: 我知道
Paul Chiusano和Rúnar Óli写了一本很棒的书Scala函数式编程。他们在书中提到了一个在Scala社区中鲜有人谈及的概念 - Transducers。
在Clojure社区中,转换器受到了一些 更多的 关注。我将为您翻译编程相关内容。以下是需要翻译的文本:
来自Scala函数式编程(FPiS)和Clojure的transducers的流转换器非常相似。它们是将“机器”(步骤函数)用于处理输入流以生成输出流的想法的一般化。FPiS的转换器称为Process
es。Rich Hickey在他关于Clojure中transducers的介绍性演讲中也使用了术语process。
FPiS转换器的设计基于Mealy机器。据说Mealy机器具有以下特点:
transition function T : (S, I) -> S
output function G : (S, I) -> O
这些函数可以融合在一起形成:
step: (S, I) -> (S, O)
在这里,很容易看出步进函数作用于机器的当前状态和下一个输入项,以产生机器的下一个状态和输出项。
FPiS中的组合器之一使用了这样的步进函数:
trait Process[I, O] {
...
def loop[S, I, O](z: S)(f: (I,S) => (O,S)): Process[I, O]
...
}
这个loop
函数本质上就是Rickey在这张幻灯片中提到的种子左规约。
两者都可以用于许多不同的上下文(如列表、流、通道等)。
在FPiS变换器中,进程类型为:
trait Process[I, O]
它只知道它的输入元素和输出元素。
在Clojure中,情况类似。Hickey称之为"完全解耦"。
这两种类型的转换器都可以组合使用。
FPiS使用“管道”运算符
map(labelHeavy) |> filter(_.nonFood)
comp
。(comp
(filtering non-food?)
(mapping label-heavy))
在Clojure中:
reducer: (whatever, input) -> whatever
transducer: reducer -> reducer
// The main type is
trait Process[I, O]
// Many combinators have the type
Process[I, O] ⇒ Process[I, O]
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]
reduced
的角色。两者都支持提前终止。Clojure使用了一个特殊的值叫做reduced
,可以通过reduced?
谓词进行测试。
FPiS使用了一种更静态类型的方法,Process可以处于三种状态之一:Await,Emit或Halt。当"步骤函数"返回一个状态为Halt的进程时,处理函数知道要停止。
在某些方面,它们又很相似。两种类型的转换器都是按需驱动的,并且不生成中间集合。然而,我想象FPiS的转换器在管道/组合时不像内部表示那样高效,因为其转换器的内部表示方式不仅仅是"只是一个函数调用的堆栈", Hickey如此描述。不过,关于效率/性能,这只是我的猜测。
请查看 fs2
(以前称为scalaz-stream
)了解一种基于FPiS中transducers设计的、可能更高效的库。
以下是两个实现中filter
的示例:
Clojure,Hickey演讲幻灯片中:
(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)))
def filter[I](f: I ⇒ Boolean): Process[I, I] =
await(i ⇒ if (f(i)) emit(i, filter(f))
else filter(f))
正如您所见,filter
是由其他组合器(例如await
和emit
)构建而成的。
在实现Clojure转换器时,有许多需要注意的地方。这似乎是一种偏向效率的设计权衡。然而,这种缺点似乎更多地影响库生产者而不是最终用户/消费者。
reduced
值,则不能再次使用该步骤函数并输入。FPiS的转换器设计偏向于正确性和易用性。管道组合和flatMap
操作确保完成操作及时发生,并适当处理错误。这些问题对于转换器的实现者来说并不是负担。话虽如此,我想库可能不像Clojure那样高效。
无论是Clojure还是FPiS变换器都具有以下特点:
它们在底层表示上略有不同。Clojure风格的变换器似乎更注重效率,而FPiS变换器则更注重正确性和组合性。
从上面的定义中可以看出,任何具有类似以下签名的函数或可调用对象:
Stream[A] -> Stream[B]
比如说,一个能够在流中工作的映射函数,在这种情况下被称为转换器。
就是这样,非常简单。
Clojure 转换器 是一种将一个归约函数转换为另一个归约函数的函数。一个归约函数是可以与reduce一起使用的函数。也就是说,如果 Clojure 有签名,它会有一个签名。
(x, a) -> x
在英语中,假设有一个起始集合x
,并且正在减少集合中的“下一件事”a
,我们的减少函数返回“正在构建的集合的下一次迭代”。
因此,如果这是减少函数的签名,那么变换器的签名为
((x, a) -> x) -> ((x, b) -> x)
core.async
通道的添加,Rich Hickey和他的朋友们发现自己需要重新实现所有标准集合函数以使用通道(map
、filter
、take
等)。RH想知道是否有更好的方法,在思考如何将这些不同的集合处理函数的逻辑与手头的集合类型的机制 解耦。确切地解释转换器如何做到这一点可能超出了这个问题的范围,所以我会回到重点。但是,如果您感兴趣,有很多易于查找和理解的文献可供参考。