Function Programming in Scala (FPiS) 一书中的流转换器和 Clojure 的转换器非常相似。它们是使用“机器”(阶跃函数)将输入流处理成输出流的概念的概括。 FPiS 的传感器称为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 中,情况类似。 Hickey 称之为"fully decoupled"。
作曲
两种类型的换能器都可以组合。
FPiS 使用“管道”运算符
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 的表示不仅仅是底层的功能。它是一个案例类(代数数据类型),有 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 的角色。
- Emit 代替调用 Clojure 中的下一步函数。
提前终止
两者都支持提前终止。 Clojure 使用称为 reduced 的特殊值来执行此操作,可以通过 reduced? 谓词对其进行测试。
FPiS 使用更静态类型的方法,进程可以处于 3 种状态之一:等待、发射或暂停。当“步进函数”返回状态为 Halt 的进程时,处理函数知道停止。
效率
在某些方面,它们再次相似。这两种类型的转换器都是需求驱动的,不会生成中间集合。但是,我想 FPiS 的传感器在流水线/组合时效率不高,因为内部表示超过 "just a stack of function calls" as Hickey puts it。我只是在这里猜测效率/性能。
查看 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 是由其他组合子(例如 await 和 emit)在这里构建的。
安全
在实现 Clojure 转换器时,有许多地方必须小心。这似乎是一种有利于效率的设计权衡。然而,这一不利因素似乎主要影响图书馆生产商,而不是最终用户/消费者。
- 如果传感器从嵌套的步骤调用中获得
reduced 值,则它绝不能使用输入再次调用该步骤函数。
- 需要状态的转换器必须创建唯一的状态,并且不能有别名。
- 所有阶跃函数都必须具有不接受输入的 arity-1 变体。
- 传感器的完成操作必须调用其嵌套的完成操作,只调用一次,然后返回它返回的内容。
FPiS 的换能器设计有利于正确性和易用性。管道组合和flatMap 操作确保完成操作迅速发生并正确处理错误。这些问题对转换器的实现者来说不是负担。也就是说,我认为该库可能不如 Clojure 的高效。
总结
Clojure 和 FPiS 传感器都有:
- 相似的起源
- 能够在不同的上下文中使用(列表、流、通道、文件/网络 io、数据库结果)
- 需求驱动/提前终止
- 最终确定/完成(为了资源安全)
-
tasty :)
它们的基本表示方式有所不同。 Clojure 风格的转换器似乎有利于效率,而 FPiS 转换器有利于正确性和组合性。