【问题标题】:understanding clojure transducers pitfalls了解 clojure 转换器的陷阱
【发布时间】:2017-05-26 17:37:55
【问题描述】:

clojure 参考 contains 以下有关传感器的 cmets,似乎在说明编写和使用传感器的安全性很重要:

如果您有应用转换器的新上下文,则需要注意一些一般规则:

  • 如果阶跃函数返回一个减小的值,则可转换过程不得再向阶跃函数提供任何输入。这 减少的值必须在完成之前用 deref 解包。

  • 完成过程必须对最终累加值恰好调用一次完成操作。

  • 转换过程必须封装对调用转换器返回的函数的引用 - 这些可能是有状态的和不安全的 跨线程使用。

您能否通过一些例子来解释这些案例的含义?另外,在这种情况下,“上下文”指的是什么?

谢谢!

【问题讨论】:

    标签: clojure


    【解决方案1】:

    如果阶跃函数返回一个减小的值,则可转换过程不得再向阶跃函数提供任何输入。减少的值必须在完成之前用 deref 解包。

    这种情况的一个例子是take-while 传感器:

    (fn [rf]
      (fn
        ([] (rf))
        ([result] (rf result))
        ([result input]
          (if (pred input)
            (rf result input)
            (reduced result)))))
    

    如您所见,它可以返回一个reduced 值,这意味着没有意义(实际上这将是一个错误)为此类步进函数提供更多输入 - 我们知道已经不会产生更多值.

    例如,在使用odd? 谓词处理(1 1 3 5 6 8 7) 输入集合时,一旦我们达到值6take-while odd? 转换器创建的步进函数将不再返回值。

    完成过程必须对最终累加值恰好调用一次完成操作。

    这是一个传感器返回有状态步进函数的场景。 partition-by 传感器就是一个很好的例子。例如,当转导过程使用(partition-by odd?) 处理(1 3 2 4 5 2) 时,它将产生((1 3) (2 4) (5) (6 8))

    (fn [rf]
      (let [a (java.util.ArrayList.)
            pv (volatile! ::none)]
        (fn
          ([] (rf))
          ([result]
             (let [result (if (.isEmpty a)
                            result
                            (let [v (vec (.toArray a))]
                              ;;clear first!
                              (.clear a)
                              (unreduced (rf result v))))]
               (rf result)))
          ([result input]
            (let [pval @pv
                  val (f input)]
              (vreset! pv val)
              (if (or (identical? pval ::none)
                      (= val pval))
                (do
                  (.add a input)
                  result)
                (let [v (vec (.toArray a))]
                  (.clear a)
                  (let [ret (rf result v)]
                    (when-not (reduced? ret)
                      (.add a input))
                    ret))))))))
    

    如果你看一下实现,你会注意到 step 函数不会返回它的累积值(存储在 a 数组列表中),直到谓词函数返回不同的结果(例如,在一系列奇数之后numbers 它将收到一个偶数,它将返回一系列累积的奇数)。问题是如果我们到达源数据的末尾 - 将没有机会观察谓词结果值的变化,并且不会返回累积值。因此,可转换过程必须调用 step 函数(arity 1)的完成操作,以便返回其累积结果(在我们的例子中为 (6 8))。

    转换进程必须封装对调用转换器返回的函数的引用 - 这些可能是有状态的,并且对于跨线程使用不安全。

    当通过传递源数据和转换器实例来执行可转换过程时,它将首先调用转换器函数以产生步进函数。 The transducer is a function of the following shape:

    (fn [xf]
      (fn ([] ...)
          ([result] ...)
          ([result input] ...)))
    

    因此,可转换过程将调用此顶级函数(接受xf - 一个归约函数)以获得用于处理数据元素的实际阶跃函数。问题是可转换过程必须保持对该阶跃函数的引用,并使用相同的实例来处理来自特定数据源的元素(例如,生成的阶跃函数实例partition-by 转换器必须用于处理整个输入序列,因为它如您在上面看到的那样保持其内部状态)。使用不同的实例来处理单个数据源会产生不正确的结果。

    类似地,由于相同的原因,可转导流程无法重用步进函数实例来处理多个数据源 - 步进函数实例可能是有状态的,并为处理特定数据源保留内部状态。当 step 函数用于处理另一个数据源时,该状态将被破坏。

    也不能保证步进函数的实现是否是线程安全的。

    “上下文”在此上下文中指的是什么?

    “应用换能器的新环境”是指实现一种新型的可转导过程。 Clojure 提供了与集合一起工作的可转导过程(例如intosequence)。 core.async 库chan 函数(它的一个参数)接受一个transducible 实例作为参数,它通过将一个transducible 应用到消耗的值来产生一个异步的transducible 过程来产生值(可以从通道中消耗)。

    例如,您可以创建一个可转换的过程来处理在套接字上接收到的数据,或者您自己的 observables 实现。

    他们可以使用转换器来转换数据,因为转换器在数据来自何处(套接字、流、集合、事件源等)时是不可知的 - 它只是一个使用单个元素调用的函数。

    他们也不关心(也不知道)应该如何处理他们生成的结果(例如,是否应该将其附加到结果序列(例如conj)?是否应该通过网络发送?插入到数据库中?) - 它是通过使用由 step 函数(上面的rf 参数)捕获的归约函数来抽象的。

    因此,我们不是创建一个仅使用conj 或将元素保存到数据库的步进函数,而是传递一个具有该操作的特定实现的函数。您的可转导过程定义了该操作是什么。

    【讨论】:

    • 如果我理解正确的话,可能值得补充的是,一个可转换的过程负责数据源的任何初始化和最终确定
    猜你喜欢
    • 2019-07-19
    • 2014-09-19
    • 2012-04-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多