【问题标题】:What are the similarities and differences between Scala Transducers and Clojure Transducers?Scala Transducers 和 Clojure Transducers 有什么异同?
【发布时间】:2015-01-07 10:07:39
【问题描述】:

Paul ChiusanoRúnar Óli 写了一本很棒的书Functional programming in Scala。在其中,他们提到了 Scala 社区中一个很少被引用的概念——Transducers。

在 Clojure 社区 - Transducers 获得 little more press

我的问题是:Scala 转换器**(来自《Scala 中的函数式编程》一书)和 Clojure 转换器有什么异同?**

假设:

我知道

  1. 传感器是their concept in Electrical Engineering 的常用说法

  2. 有一个预先存在的概念in Computer Science called a Finite State Transducer

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

  4. already a history其他技术书籍,如SICP adopting the word Transducers

【问题讨论】:

    标签: scala clojure transducer transducer-machines


    【解决方案1】:

    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 是由其他组合子(例如 awaitemit)在这里构建的。

    安全

    在实现 Clojure 转换器时,有许多地方必须小心。这似乎是一种有利于效率的设计权衡。然而,这一不利因素似乎主要影响图书馆生产商,而不是最终用户/消费者。

    • 如果传感器从嵌套的步骤调用中获得 reduced 值,则它绝不能使用输入再次调用该步骤函数。
    • 需要状态的转换器必须创建唯一的状态,并且不能有别名。
    • 所有阶跃函数都必须具有不接受输入的 arity-1 变体。
    • 传感器的完成操作必须调用其嵌套的完成操作,只调用一次,然后返回它返回的内容。

    FPiS 的换能器设计有利于正确性和易用性。管道组合和flatMap 操作确保完成操作迅速发生并正确处理错误。这些问题对转换器的实现者来说不是负担。也就是说,我认为该库可能不如 Clojure 的高效。

    总结

    Clojure 和 FPiS 传感器都有:

    • 相似的起源
    • 能够在不同的上下文中使用(列表、流、通道、文件/网络 io、数据库结果)
    • 需求驱动/提前终止
    • 最终确定/完成(为了资源安全)
    • tasty :)

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

    【讨论】:

      【解决方案2】:

      我不是特别熟悉 Scala 的传感器概念或该术语的普遍性,但根据您在上面发布的文本的 sn-p(以及我对传感器的知识),我可以说:

      • 它们非常不同
      • 它们的相似之处仅在于它们都与一个集合如何转换为另一个集合有关

      关于 Scala 转换器我能说的:

      从上面的定义看来,任何带有签名的函数或可调用对象都大致类似于

      Stream[A] -> Stream[B]
      

      因此,例如,在这种情况下,处理流的映射函数将被视为转换器。

      就是这样;真的很简单。

      Clojure 传感器:

      Clojure transducer 是将一个归约函数转换为另一个的函数。 reducing 函数可以与reduce 一起使用。也就是说,如果 Clojure 有签名,它就会有一个签名

      (x, a) -> x
      

      在英语中,给定一些起始集合x,并且集合中的“下一件事”a 被归约,我们的归约函数返回“正在构建的集合的下一次迭代”。

      所以,如果这是一个归约函数的签名,那么换能器就有签名

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

      转换器被添加到 Clojure 的原因是,通过添加或 core.async 通道,Rich Hickey 和朋友发现他们重新实现了所有标准收集功能以使用通道(mapfiltertake , ETC。)。 RH 想知道这里有没有更好的方法,于是开始工作,从手头的集合类型的机制中思考如何decomplect 这些各种集合处理函数的逻辑。然而,我认为准确解释传感器如何做到这一点超出了这个问题的范围,所以我会回到正题。但是,如果您有兴趣,可以很容易地找到和阅读大量有关这方面的文献。

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

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

      虽然 Scala 转换器是 Streams 的集合处理函数(与其他 Scala 集合相对),但 Clojure 的转换器实际上是一种机制,用于统一跨不同集合类型的集合处理函数的实现。因此,一种说法是,如果 Scala 具有 Clojure 的转换器概念,Scala 的转换器概念可以根据 Clojure 的转换器概念来实现,后者是更抽象/通用的处理函数,可重用于多种集合类型。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-11-18
        • 2015-12-09
        • 1970-01-01
        • 2014-03-15
        • 2014-03-11
        • 2010-10-07
        • 1970-01-01
        相关资源
        最近更新 更多