Clojure 传感器行为

Clojure transducers behavior

有了新的 clojure 1.7,我决定了解我可以在哪里使用转换器。我知道他们可以提供什么好处,但我找不到编写自定义转换器的正常示例并进行解释。

好的,我试着测试发生了什么。我打开了 clojure 文档。并且有示例使用 xf 作为参数。第一:这个 xf 或 xfrom 是什么意思? 这玩意产生身份转换器。

(defn my-identity [xf]
  (fn 
   ([]
     (println "Arity 0.")
     (xf))
   ([result]
     (println "Arity 1: " result " = " (xf result)) 
     (xf result))
   ([result input]
     (println "Arity 2: " result input " = " (xf result input)) 
     (xf result input))))

我从文档示例中获取了变量的命名 [result input]。 我认为这就像在 reduce 函数中,其中 result 是减少的部分,而 input 是新的集合元素。

所以当我制作 (transduce my-identity + (range 5)) 时,我得到了 10 我期望的结果。 然后我读到 eduction,但我不明白它是什么。反正 我做了 (eduction my-identity (range 5)) 并得到:

Arity 2:  nil 0  =  nil
Arity 2:  nil 1  =  nil
Arity 1: nil  =  nil
(0 0 1 1)

每个项目都重复了,因为我在 println 语句中调用了 xf。 为什么它复制每个项目两次? 为什么我没有? 在进行扣除时我总是会得到 nil 吗? 我可以转述这种行为吗?

反正我做了

> (reduce + (eduction my-identity (range 5))
clojure.core.Eduction cannot be cast to clojure.lang.IReduce

好的,结果是 Eduction,它不可简化,但像列表一样打印。为什么它不可还原?当我输入 (doc eduction) 时,我得到

Returns a reducible/iterable application of the transducers
to the items in coll.

(transduce xform f coll)(reduce f (eduction xfrom coll)) 不应该一样吗?

我做了

> (reduce + (sequence my-identity (range 5))
20

我当然得到 20 因为重复。我又觉得应该是 (transduce xform f coll)(reduce f (sequence xfrom coll)) 是 至少在这样一个没有任何状态传感器的小例子中总是相等的。这是愚蠢的,他们不是,或者我错了?

好的,然后我尝试 (type (sequence my-identity (range 5))) 并得到 clojure.lang.LazySeq 我想,这很懒惰,但是当我尝试使用 first 元素时 clojure 一次计算了所有序列。

所以我的总结:

1) xf 或 xform 是什么意思?

2) 为什么我得到 nil 作为 result 参数而 eductionsequence

3) 我能否始终确定它将是 nileductionsequence

4) 什么是 eduction 以及它不可简化的惯用想法是什么?或者如果是,那我该如何减少它?

5) 为什么我在 sequenceeduction 时会出现副作用?

6) 我可以用转换器创建真正的惰性序列吗?

问题很多,先回答几个:

  1. 是的,xf==xform是一个“换能器”。

  2. 您的 my-identity 函数无法编译。你有一个参数然后 函数的多个其他参数。我相信你忘记了一个 (fn ...).

  3. 你对身份转换器的论证叫做xf。然而,这是 通常称为rf,意思是“约简函数”。现在令人困惑的部分是 xf 也是约简函数(因此 comp 有效)。然而, 令人困惑的是,你称它为 xf 而你应该称它为 rf.

  4. 转换器通常是“构建的”,因为它们可能是有状态的 and/or 传递的参数。在你的情况下,你不需要构建它,因为它是 简单,没有状态甚至参数。但是请注意,您会 通常将您的函数包装在另一个 fn 返回函数中。这意味着你会 必须调用 (my-identity) 而不是将其作为 my-identity 传递。 同样,这里很好,只是有点不合常理,可能令人困惑。

  5. 让我们先继续假设你的 my-identity 换能器是 正确(不是,稍后我会解释是怎么回事)。

  6. eduction 比较少用。它创建了一个“过程”。 IE。您可以 运行 一遍又一遍地查看结果。基本上,只是 就像你有列表或向量来保存你的项目, education 将“持有” 应用传感器的结果。请注意,要实际做任何事情,你 仍然需要一个rf(减少功能)。

  7. 开始的时候我觉得想到归约函数是有帮助的 作为 conj(或实际上 conj!)或在您的情况下 +.

  8. 你的 eduction 打印它产生的元素,因为它实现了 Iterableprintln 或您的 REPL 调用。它只是打印出每个 您使用 arity 2 调用添加到换能器中的元素。

  9. 您拨打 (reduce + (eduction my-identity (range 5))) 的电话打不通 因为 Eduction(在 eduction 中构造的对象)只实现 IReduceInitIReduceInit 顾名思义 需要 初始 价值。所以这会起作用:(reduce + 0 (eduction my-identity (range 5)))

  10. 现在如果你运行上面reduce我建议你会看到一些非常 有趣的。它打印 10。即使你的 eduction 早些时候打印了 (0 0 1 1 2 2 3 3 4 4)(如果你加在一起是 20)。这是怎么回事?

  11. 如前所述,您的换能器有缺陷。它不能正常工作。这 问题是你调用你的 rf 然后在你的 元数 2 函数。在 clojure 中,东西是不可变的,除非它以某种方式 出于优化目的内部可变 :)。 这里的问题是有时 clojure 使用变异并且你得到重复项 即使您从未正确捕获第一次调用的结果 (rf) 在你的 arity 2 函数中(作为你的 println 的参数)。

让我们修复您的功能 ,但将第二个 rf 调用留在那里:

  (defn my-identity2 [rf]
    (fn
      ([]
       (println "Arity 0.")
       (rf))
      ([result]
       {:post [(do (println "Arity 1 " %) true)]
        :pre  [(do (println "Arity 1 " result) true)]}
       (rf result))
      ([result input]
       {:post [(do (println "Arity 2 " %) true)]
        :pre  [(do (println "Arity 2 " result input) true)]}
       (rf (rf result input) input))))

注:

  • 如前所述,我将 xf 重命名为 rf
  • 现在我们可以看到您使用了您 rf 的结果并将其传递给 rf 的第二次调用。 此换能器不是身份换能器,而是 将每个元素加倍

仔细观察:

(transduce my-identity + (range 5));; => 10
(transduce my-identity2 + (range 5));; => 20

(count (into '() my-identity (range 200)));; => 200
(count (into  [] my-identity (range 200)));; => 400

(count (into '() my-identity2 (range 200)));; => 400
(count (into  [] my-identity2 (range 200)));; => 400

(eduction my-identity  (range 5));;=> (0 0 1 1 2 2 3 3 4 4)
(eduction my-identity2 (range 5));;=> (0 0 1 1 2 2 3 3 4 4)

(into '() my-identity  (range 5));;=> (4 3 2 1 0)
(into  [] my-identity  (range 5));;=> [0 0 1 1 2 2 3 3 4 4]
(into '() my-identity2 (range 5));;=> (4 4 3 3 2 2 1 1 0 0)


(reduce + 0 (eduction my-identity (range 5)));;=> 10
(reduce +   (sequence my-identity (range 5)));;=> 20

(reduce + 0 (eduction my-identity2 (range 5)));;=> 20
(reduce +   (sequence my-identity2 (range 5)));;=> 20

回答您的问题:

  1. eduction 并没有真正将 nil 作为 result 参数传递 减少。它只会在调用 Iterable 的打印时得到 nil 接口.
  2. nil真的来自TransformerIterator,这是一个特殊的class 为换能器创建。如您所见,此 class 也用于 sequence。 正如文档所述:

The resulting sequence elements are incrementally computed. These sequences will consume input incrementally as needed and fully realize intermediate operations. This behavior differs from the equivalent operations on lazy sequences.

您收到 nil 作为 result 参数的原因是因为迭代器没有结果集合来保存到目前为止迭代的元素。它只是遍历每个元素。没有状态正在累积。

您可以在此处查看 TransformerIterator as 和内部 class 使用的缩减函数:

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/TransformerIterator.java

执行 CTRL+f 并输入 xf.invoke 以查看如何调用您的传感器。

sequence 函数并不像真正的惰性序列那样惰性,但我 认为这解释了你的这部分问题:

Are Clojure transducers eager?

sequence 只是递增地计算传感器的结果。没有什么 否则。

最后,带有一些调试语句的正确身份函数:

(defn my-identity-prop [rf]
  (fn
    ([]
     (println "Arity 0.")
     (rf))
    ([result]
     (let [r (rf result)]
       (println "my-identity(" result ") =" r)
       r))
    ([result input]
     (let [r (rf result input)]
       (println "my-idenity(" result "," input ") =" r)
       r))))